diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000000..6b833803b2e --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,191 @@ +# Coding Guidelines + +## Introduction + +These are VS Code coding guidelines. Please also review our [Source Code Organisation](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) page. + +## Indentation + +We use tabs, not spaces. + +## Naming Conventions + +* Use PascalCase for `type` names +* Use PascalCase for `enum` values +* Use camelCase for `function` and `method` names +* Use camelCase for `property` names and `local variables` +* Use whole words in names when possible + +## Types + +* Do not export `types` or `functions` unless you need to share it across multiple components +* Do not introduce new `types` or `values` to the global namespace + +## Comments + +* When there are comments for `functions`, `interfaces`, `enums`, and `classes` use JSDoc style comments + +## Strings + +* Use "double quotes" for strings shown to the user that need to be externalized (localized) +* Use 'single quotes' otherwise +* All strings visible to the user need to be externalized + +## Style + +* Use arrow functions `=>` over anonymous function expressions +* Only surround arrow function parameters when necessary. For example, `(x) => x + x` is wrong but the following are correct: + +```javascript +x => x + x +(x, y) => x + y +(x: T, y: T) => x === y +``` + +* Always surround loop and conditional bodies with curly braces +* Open curly braces always go on the same line as whatever necessitates them +* Parenthesized constructs should have no surrounding whitespace. A single space follows commas, colons, and semicolons in those constructs. For example: + +```javascript +for (let i = 0, n = str.length; i < 10; i++) { + if (x < 10) { + foo(); + } +} + +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/.npmrc b/.npmrc index eff6e8cdba9..22256e5d8e7 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" -target="32.2.5" -ms_build_id="10579404" +target="32.2.6" +ms_build_id="10629634" runtime="electron" build_from_source="true" legacy-peer-deps="true" diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index c152f3ad6c6..2e4c8d82aac 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"October 2024\"" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"November 2024\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 0b0725741fd..60e03000ee8 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"October 2024\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"November 2024\"\n\n$MINE=assignee:@me" }, { "kind": 1, diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js index 3816db385a0..102c5518d9b 100644 --- a/build/azure-pipelines/common/publish.js +++ b/build/azure-pipelines/common/publish.js @@ -500,14 +500,20 @@ async function processArtifact(artifact, filePath) { 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...', JSON.stringify(asset, undefined, 2)); - await (0, retry_1.retry)(async (attempt) => { + 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; - await scripts.storedProcedure('createAsset').execute('', [version, asset, true]); + const { resource: result } = await scripts.storedProcedure('createAsset').execute('', [version, asset, true]); + return result; }); - log('Asset successfully created'); + if (result === 'already exists') { + log('Asset already exists!'); + } + else { + log('Asset successfully created: ', JSON.stringify(asset, undefined, 2)); + } } // 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 a3760c03434..ab9270e177d 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -834,16 +834,21 @@ async function processArtifact( } const asset: Asset = { platform, type, url, hash: hash.toString('hex'), sha256hash: sha256hash.toString('hex'), size, supportsFastUpdate: true }; - log('Creating asset...', JSON.stringify(asset, undefined, 2)); + log('Creating asset...'); - await retry(async (attempt) => { + 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; - await scripts.storedProcedure('createAsset').execute('', [version, asset, true]); + const { resource: result } = await scripts.storedProcedure('createAsset').execute<'ok' | 'already exists'>('', [version, asset, true]); + return result; }); - log('Asset successfully created'); + if (result === 'already exists') { + log('Asset already exists!'); + } else { + log('Asset successfully created: ', JSON.stringify(asset, undefined, 2)); + } } // It is VERY important that we don't download artifacts too much too fast from AZDO. diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 92c742d3b50..17ec96faa2a 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -fed2c175e3b63682e7b939b704b524e5af7e664f6b020cfec4895b6258190f7c *chromedriver-v32.2.5-darwin-arm64.zip -fa40996a7d0c5b830d101a0b6511300ff4477995b5af86b1e8be5e21341caa8e *chromedriver-v32.2.5-darwin-x64.zip -9881960752aa2dee6577ab4c312d0a5e69f596c209cab32e2c5bd3b32222c79a *chromedriver-v32.2.5-linux-arm64.zip -864754d188b9e7a15abd97a3b41d7d53db300ef3401b76fa81633c298e3b09f0 *chromedriver-v32.2.5-linux-armv7l.zip -75e0d1f19e61caaa2b0f50966623f2948fffc864138f6ee8fe38791b733fd182 *chromedriver-v32.2.5-linux-x64.zip -360695b3ac1f1faa9caf0c670d81ed065e230822d77343d7dd6aa2988b2ecf99 *chromedriver-v32.2.5-mas-arm64.zip -03778e3fda6d50a9f122a3e5e4a5723953918f7b67ef5a41578ae1c41a3c2579 *chromedriver-v32.2.5-mas-x64.zip -807a45c2c40a3da025a7fd394d2ef34ed78b0719f2af924694200818cca1c0ca *chromedriver-v32.2.5-win32-arm64.zip -37da8dd36a36dbafa3b20101c5a3066ca03a36db45e478872b1c88231007f35e *chromedriver-v32.2.5-win32-ia32.zip -8f624e3969185ea47840c3f65d6ff2e12a2d7d8b6251b15c8b51ff690bd0a517 *chromedriver-v32.2.5-win32-x64.zip -2424f6f47c8c2e59546fad9d93324772ea1b6d290ce4672b6689ffc45c96a950 *electron-api.json -e4b455bf2339bf3945c7eb3c9faa52f1122eb9d4b97cedeced5887c01125c33d *electron-v32.2.5-darwin-arm64-dsym-snapshot.zip -016c1ec183649162c436c599bdc6f9a02a635fec4b30b99f97cc25829d70f07c *electron-v32.2.5-darwin-arm64-dsym.zip -0cc937ba248fde2098e9affdd6aa0ea2f0091ab700d8ac9b46cba4a11a0adaff *electron-v32.2.5-darwin-arm64-symbols.zip -add26413ae4b6055a37335be8a551007b8f47759ee3bed19d3ab0463c6b3422a *electron-v32.2.5-darwin-arm64.zip -3f8802fba7a8274308b98d4ac50a730f3e75ed447cca70ae63395d6ebd1d2bc9 *electron-v32.2.5-darwin-x64-dsym-snapshot.zip -f7a427b0b884be02f89b7a5b9a8dc9a6d573cf0c3a342e18483b4719a1c9a5b0 *electron-v32.2.5-darwin-x64-dsym.zip -85b68c3869332a33d5ad3d90639b5d8e1777e67df9ae23ac85d1fa412d1ae98c *electron-v32.2.5-darwin-x64-symbols.zip -67bb4e9e02f244516f10b78b380aa19a7026e025d568878d15035debe758eef4 *electron-v32.2.5-darwin-x64.zip -8a3f519c6a31d40c9849e6641668a152e73ec210e3477d1bf2ed957ac9955b2d *electron-v32.2.5-linux-arm64-debug.zip -9c64b56211d4013643cec784b030456b6b06f01cbdb96d4acc90dd5c91b74242 *electron-v32.2.5-linux-arm64-symbols.zip -0215737ef1e8bb7ca2def455ec4abcfd5743a9fa7af0b7cfa608295a54f5aae3 *electron-v32.2.5-linux-arm64.zip -8a3f519c6a31d40c9849e6641668a152e73ec210e3477d1bf2ed957ac9955b2d *electron-v32.2.5-linux-armv7l-debug.zip -0f84c7b3be36ab416a14353ec92f439d1acd9f30be7308ad3b3b514425d7ee9c *electron-v32.2.5-linux-armv7l-symbols.zip -e692c6d20cdea3f0e26519ac660b762e8119962f6b6e0032fe2a7fb73f4d205c *electron-v32.2.5-linux-armv7l.zip -1e39d6c04ab451072539aba551df6900dd6f423678f491c3437643d3f18cf657 *electron-v32.2.5-linux-x64-debug.zip -ab3c4a7beb1e5f18259142a70eecaa5c06eb72a23a692aecee87598dc413ea58 *electron-v32.2.5-linux-x64-symbols.zip -6d92ff595786c3a8926684c6983fdcb09b20dc34b37a1affb4c4dbfb659fee1e *electron-v32.2.5-linux-x64.zip -0fac69cb73abbd8a3fefdc80008053d68f0cefa3d5a79f1a849ae6dc374d2778 *electron-v32.2.5-mas-arm64-dsym-snapshot.zip -cbced9a83753637506b3c0f1c57b42b19dffb4494746e0a2b73c8bce45f4b5ae *electron-v32.2.5-mas-arm64-dsym.zip -d5b2f03a476a96d8e87670c2e07a84b872881bc49f327145a80b3f063490e0e2 *electron-v32.2.5-mas-arm64-symbols.zip -b95e7232b208568e8d7f278eb341cdf88b8c5106403f5abedc495305e3b6744f *electron-v32.2.5-mas-arm64.zip -5031bafcb557ad61a75f925147917b575428671a252ebbf6b77f53410ae3e434 *electron-v32.2.5-mas-x64-dsym-snapshot.zip -206e023d61e299289869f96ad218863a14a5e71f05b48f16de1cc48e53ba028f *electron-v32.2.5-mas-x64-dsym.zip -7bdb96b90ffa22ef1156c508f957c66ad1033b3be1cb1a3b9dd7bb98c9088696 *electron-v32.2.5-mas-x64-symbols.zip -2f46aa2c8a9a7f28a1bd148a41d170389396c457634a896bef38e472b5e66d9c *electron-v32.2.5-mas-x64.zip -f614582a35d4e4d68ea6861d35d49062908a202e208cf09354ef66982c540f7f *electron-v32.2.5-win32-arm64-pdb.zip -c8f8375ab562970ca02ba83755c69fa104a2621ad481d25f93cec797b64bf6bc *electron-v32.2.5-win32-arm64-symbols.zip -48b81d28fdceb4ab3ca27650d79bab910a1a19dbda72271882bfdc877c71975f *electron-v32.2.5-win32-arm64-toolchain-profile.zip -b3d6ed4c2ccc567bcd4f405ed20b33e4ba9dd0bcfb54cb99017a0ed2eb8ec1ef *electron-v32.2.5-win32-arm64.zip -2d5ca8fc59b5cbb8799dd1ee2916725ed7f12d2b6264062e19b1378aca7b326a *electron-v32.2.5-win32-ia32-pdb.zip -fd8de6c8ccf7094bf83d5ce86866d9e73b082dc4f28250bc939431d79a46a2d9 *electron-v32.2.5-win32-ia32-symbols.zip -48b81d28fdceb4ab3ca27650d79bab910a1a19dbda72271882bfdc877c71975f *electron-v32.2.5-win32-ia32-toolchain-profile.zip -b426125e315e2819c60b39b2e443d8a76e5b1fc320595b5b560b00e931c6a0d1 *electron-v32.2.5-win32-ia32.zip -8bb5edd099cc5ef155179928fba6bc3d78b7111eed8e6b9727bc068c787ec235 *electron-v32.2.5-win32-x64-pdb.zip -329eb0d32cb6c03c617dc5b5378c97a6f359b63492c00207a73ec0dd427d70a4 *electron-v32.2.5-win32-x64-symbols.zip -48b81d28fdceb4ab3ca27650d79bab910a1a19dbda72271882bfdc877c71975f *electron-v32.2.5-win32-x64-toolchain-profile.zip -0ae7add4862e34675384e7119902e6d2384d2712a5ebe98d8994b45dfa6ead12 *electron-v32.2.5-win32-x64.zip -146a192ac5e05bbd8172e3107cd4a1cae2b4882c98272ec735a2628889803104 *electron.d.ts -3f1f2db3beade0ef71ba4e8c1549368133f9aad1f377db91aba3dbac773dc770 *ffmpeg-v32.2.5-darwin-arm64.zip -441b3459f3e684f1444a7a88b56d88efc7ed0f466efc71b4daa32b467442231a *ffmpeg-v32.2.5-darwin-x64.zip -3f1eafaf4cd90ab43ba0267429189be182435849a166a2cbe1faefc0d07217c4 *ffmpeg-v32.2.5-linux-arm64.zip -3db919bc57e1a5bf7c1bae1d7aeacf4a331990ea82750391c0b24a046d9a2812 *ffmpeg-v32.2.5-linux-armv7l.zip -fe7d779dddbfb5da5999a7607fc5e3c7a6ab7c65e8da9fee1384918865231612 *ffmpeg-v32.2.5-linux-x64.zip -20f83028f1e263287bc83ec817d548a3e9c160aeadeb97bea0b40b6c256e6b2f *ffmpeg-v32.2.5-mas-arm64.zip -921551e865c81047259b77325c5d1bfc1cd29463c2eab7d9b37bb2bb507e9e25 *ffmpeg-v32.2.5-mas-x64.zip -713563936304f814324874686f19bcdc6b7d6472e8d4f4ab459970a059123d7a *ffmpeg-v32.2.5-win32-arm64.zip -616b736527e7a2b07fd62d8cff7c62d3a2f41725c4d45a6075c9354b5d758085 *ffmpeg-v32.2.5-win32-ia32.zip -517786aabef79bb55fd932676ad3ccacd776fac47b230067f3b939207ad7f204 *ffmpeg-v32.2.5-win32-x64.zip -d66731d99d7a4f586a86f3eea4b5807e7601da8d7b697a6ae0edff296b6a2206 *hunspell_dictionaries.zip -f5a90b865c32194e2e593c790ad05fb11f2011208796a0ad5438ac03792a3da0 *libcxx-objects-v32.2.5-linux-arm64.zip -dfcbae6c5b65397ec7ecf56fe9675ac2ca8a6507cdfe3abee10acd36c55536ad *libcxx-objects-v32.2.5-linux-armv7l.zip -a358503519eb66da7ae35d5ac0cf47482c045e2c03a6a0dd70e77e94f44d95c9 *libcxx-objects-v32.2.5-linux-x64.zip -7c1d5dff2dc9e9a450ec29da808ef1720bf129b71e7418b8815e8525da65f899 *libcxx_headers.zip -875d1697b3cde375ed63cb56104b1c53157bdd611fb3938f086be9579177bce2 *libcxxabi_headers.zip -1639adba066f123dbbf9d632a3ea786e7bc8e9027ff103972207e818e08946fb *mksnapshot-v32.2.5-darwin-arm64.zip -de90ae0b520d8ff3a175e632f627ccc413260d4e19e40c09cd9b1b75b4482611 *mksnapshot-v32.2.5-darwin-x64.zip -62029765e6b48ceee36a6b6c9e3252b386271c22839bafd21dabd2f3f1f19901 *mksnapshot-v32.2.5-linux-arm64-x64.zip -be21fd7442a9d599d70de8fb8776fe778b979fe85a47bfb92e57ac3158d2abcc *mksnapshot-v32.2.5-linux-armv7l-x64.zip -9a4f258c13b69846f405344e400a9dd149c943c39c04cf1799b6dc19cc223449 *mksnapshot-v32.2.5-linux-x64.zip -b53b42662802239c6f49e5c9805ed05463197ddbd8ca35436389adb4420e4ebb *mksnapshot-v32.2.5-mas-arm64.zip -f32544d647db6cdea6e538d29f05e65b01b7d8d98b8904ed7961e5ed7204cc4b *mksnapshot-v32.2.5-mas-x64.zip -8fc8daed10eea093d185a4bda35dc95789e8f99932193b6b9ff1ff5411f39c38 *mksnapshot-v32.2.5-win32-arm64-x64.zip -ce5d40a5e795be00faefa0383e32a96b8d41d3c1c050d2b86944084cb03de9db *mksnapshot-v32.2.5-win32-ia32.zip -7e9451ad4308f9d7a3dcd8e3ce55d029f82128a6b1370adb086e8f854d58d4e9 *mksnapshot-v32.2.5-win32-x64.zip +bb4164f7b554606b2c4daaf43e81bf2e2b5cf0d4441cfdd74f04653237fcf655 *chromedriver-v32.2.6-darwin-arm64.zip +a0fc3df1c6cd17bfe62ffbb1eba3655ca625dea5046e5d2b3dbb0e9e349cd10e *chromedriver-v32.2.6-darwin-x64.zip +671d6dab890747ea73ba5589327eef7612670950a20e5f88c7d8a301b5491e26 *chromedriver-v32.2.6-linux-arm64.zip +55bfd4e33fef1506261d4cb3074988e1970c2a762ca76a8f1197512a1766723c *chromedriver-v32.2.6-linux-armv7l.zip +d3c7a45c8c75152db927b3596f506995e72631df870b302b7dbcbd3399e54a3a *chromedriver-v32.2.6-linux-x64.zip +567f77d09708942901c6cdce6708b995f6ac779faceebb4ed383ca5003e2de4e *chromedriver-v32.2.6-mas-arm64.zip +b3a28181b1d077742f1be632a802e15b5a36a260b1cfe0e429735de9f52d074a *chromedriver-v32.2.6-mas-x64.zip +a113f5bd747b6eeb033f4d6ea2f53cf332d9b45d6340af514dd938bac7f99419 *chromedriver-v32.2.6-win32-arm64.zip +3b3237a788fad0a6be63a69b93c28b6052db23aeaa1a75d2589be15b4c2c0f2f *chromedriver-v32.2.6-win32-ia32.zip +1096c131cf8e3f98a01525e93d573eaf4fd23492d8dd78a211e39c448e69e463 *chromedriver-v32.2.6-win32-x64.zip +8e6fcf3171c3fcdcb117f641ec968bb53be3d38696e388636bf34f04c10b987d *electron-api.json +b1b20784a97e64992c92480e69af828a110d834372479b26759f1559b3da80fc *electron-v32.2.6-darwin-arm64-dsym-snapshot.zip +f11dd5a84229430ec59b4335415a4b308dc4330ff7b9febae20165fbdd862e92 *electron-v32.2.6-darwin-arm64-dsym.zip +cc96cf91f6b108dc927d8f7daee2fe27ae8a492c932993051508aa779e816445 *electron-v32.2.6-darwin-arm64-symbols.zip +fcb6bbb6aa3c1020b4045dbe9f2a5286173d5025248550f55631e70568e91775 *electron-v32.2.6-darwin-arm64.zip +d2bfeea27fc91936b4f71d0f5c577e5ad0ea094edba541dfa348948fd65c3331 *electron-v32.2.6-darwin-x64-dsym-snapshot.zip +5e7684cc12c0dee11fb933b68301d0fe68d3198d1daeadd5e1b4cf52743f79bf *electron-v32.2.6-darwin-x64-dsym.zip +6d2a7d41ab14fc7d3c5e4b35d5d425edb2d13978dcc332e781ec8b7bcfe6a794 *electron-v32.2.6-darwin-x64-symbols.zip +c0964ee5fdcefb1003ffd7ef574b07e5147856f3a94bb4335f78c409f8cf2eca *electron-v32.2.6-darwin-x64.zip +604d88b9d434ea66ddf234dd129dcef3d468b95b0da47e2f1555a682c8d0de28 *electron-v32.2.6-linux-arm64-debug.zip +f448e91df42fc84177bcd46378e49ee648f6114984fc57af4a84690a5197c23e *electron-v32.2.6-linux-arm64-symbols.zip +9e4f9345cae06e8e5679b228e7b7ac21b8733e3fcda8903e3dcbc8171c53f8be *electron-v32.2.6-linux-arm64.zip +604d88b9d434ea66ddf234dd129dcef3d468b95b0da47e2f1555a682c8d0de28 *electron-v32.2.6-linux-armv7l-debug.zip +c85d5ca3f38dc4140040bcde6a37ac9c7510bb542f12e1ffce695a35f68e3c13 *electron-v32.2.6-linux-armv7l-symbols.zip +df4b490a9c501d83c5305f20b2a9d1aa100d2e878e59ebafde477f21d35e3300 *electron-v32.2.6-linux-armv7l.zip +a5aa67da85ee318ff0770d55a506f62e6e5a10e967dfab272a94bcd91922e075 *electron-v32.2.6-linux-x64-debug.zip +671eb342a58e056f0dee5a6e9c69a6a96ee2141f81d306fa1a0e2635e22aa7c0 *electron-v32.2.6-linux-x64-symbols.zip +a3231409db7f8ac2cc446708f17e2abac0f8549c166eaab2f427e8d0f864208d *electron-v32.2.6-linux-x64.zip +8b8d0aeadcf21633216a9cce87d323ad6aa21e38ec82092cd5f65bf171be025f *electron-v32.2.6-mas-arm64-dsym-snapshot.zip +298931236955b83d174738d3325931e9672a8333bf854fc5f471168b0f7e70be *electron-v32.2.6-mas-arm64-dsym.zip +53e4c666a6f5f87aa150b1c2f34532e3711e00b0237fb103dcbef64d65979429 *electron-v32.2.6-mas-arm64-symbols.zip +bedbc78acc3bc6cb30e5fe1f133562104152022273ec21a83cd32a0eece9003f *electron-v32.2.6-mas-arm64.zip +9ba3def756c90460867d968af5417996991bf3b4b306dba4c9fbde43de2f771d *electron-v32.2.6-mas-x64-dsym-snapshot.zip +e4a3f9392934bb4ef82a214fec2ce1f319ea409b89bf03b2a2a4ab7a55688d0c *electron-v32.2.6-mas-x64-dsym.zip +55c54fd01faf5767493183001a440b837b63f8b8d64c36f7b42fa7217af36dcd *electron-v32.2.6-mas-x64-symbols.zip +1efe974cd426a288d617750c864e6edbf28495c3b851a5bc806af19cda7a274d *electron-v32.2.6-mas-x64.zip +88c50d34dc48a55a11014349d2d278f895f1615405614dbfcf27dff5f5030cf1 *electron-v32.2.6-win32-arm64-pdb.zip +b0b2211bf0f11924bcd693b6783627c7f6c9a066117bcf05c10766064c79794c *electron-v32.2.6-win32-arm64-symbols.zip +48b81d28fdceb4ab3ca27650d79bab910a1a19dbda72271882bfdc877c71975f *electron-v32.2.6-win32-arm64-toolchain-profile.zip +2b7962348f23410863cb6562d79654ce534666bab9f75965b5c8ebee61f49657 *electron-v32.2.6-win32-arm64.zip +d248ab4ec8b4a5aec414c7a404e0efd9b9a73083f7c5cb6c657c2ed58c4cbe94 *electron-v32.2.6-win32-ia32-pdb.zip +2ef98197d66d94aee4978047f22ba07538d259b25a8f5f301d9564a13649e72c *electron-v32.2.6-win32-ia32-symbols.zip +48b81d28fdceb4ab3ca27650d79bab910a1a19dbda72271882bfdc877c71975f *electron-v32.2.6-win32-ia32-toolchain-profile.zip +e85dbf85d58cab5b06cdb8e76fde3a25306e7c1808f5bb30925ba7e29ff14d13 *electron-v32.2.6-win32-ia32.zip +9d76b0c0d475cc062b2a951fbfb3b17837880102fb6cc042e2736dfebbfa0e5d *electron-v32.2.6-win32-x64-pdb.zip +3d7b93bafc633429f5c9f820bcb4843d504b12e454b3ecebb1b69c15b5f4080f *electron-v32.2.6-win32-x64-symbols.zip +48b81d28fdceb4ab3ca27650d79bab910a1a19dbda72271882bfdc877c71975f *electron-v32.2.6-win32-x64-toolchain-profile.zip +77d5e5b76b49767e6a3ad292dc315fbc7cdccd557ac38da9093b8ac6da9262d5 *electron-v32.2.6-win32-x64.zip +a52935712eb4fc2c12ac4ae611a57e121da3b6165c2de1abd7392ed4261287e2 *electron.d.ts +6345ea55fda07544434c90c276cdceb2662044b9e0355894db67ca95869af22a *ffmpeg-v32.2.6-darwin-arm64.zip +4c1347e8653727513a22be013008c2760d19200977295b98506b3b9947e74090 *ffmpeg-v32.2.6-darwin-x64.zip +3f1eafaf4cd90ab43ba0267429189be182435849a166a2cbe1faefc0d07217c4 *ffmpeg-v32.2.6-linux-arm64.zip +3db919bc57e1a5bf7c1bae1d7aeacf4a331990ea82750391c0b24a046d9a2812 *ffmpeg-v32.2.6-linux-armv7l.zip +fe7d779dddbfb5da5999a7607fc5e3c7a6ab7c65e8da9fee1384918865231612 *ffmpeg-v32.2.6-linux-x64.zip +e09ae881113d1b3103aec918e7c95c36f82b2db63657320c380c94386f689138 *ffmpeg-v32.2.6-mas-arm64.zip +ee316e435662201a81fcededc62582dc87a0bd5c9fd0f6a8a55235eca806652f *ffmpeg-v32.2.6-mas-x64.zip +415395968d31e13056cefcb589ed550a0e80d7c3d0851ee3ba29e4dcdf174210 *ffmpeg-v32.2.6-win32-arm64.zip +17f61a5293b707c984cee52b57a7e505cde994d22828e31c016434521638e419 *ffmpeg-v32.2.6-win32-ia32.zip +f9b47951a553eec21636b3cc15eccf0a2ba272567146ec8a6e2eeb91a985fd62 *ffmpeg-v32.2.6-win32-x64.zip +7d2b596bd94e4d5c7befba11662dc563a02f18c183da12ebd56f38bb6f4382e9 *hunspell_dictionaries.zip +06bca9a33142b5834fbca6d10d7f3303b0b5c52e7222fe783db109cd4c5260ed *libcxx-objects-v32.2.6-linux-arm64.zip +7ab8ff5a4e1d3a6639978ed718d2732df9c1a4dd4dcd3e18f6746a2168d353a9 *libcxx-objects-v32.2.6-linux-armv7l.zip +ba96792896751e11fdba63e5e336e323979986176aa1848122ca3757c854352e *libcxx-objects-v32.2.6-linux-x64.zip +1f858d484f4ce27f9f3c4a120b2881f88c17c81129ca10e495b50398fb2eed64 *libcxx_headers.zip +4566afb06a6dd8bd895dba5350a5705868203321116a1ae0a216d5a4a6bfb289 *libcxxabi_headers.zip +350f5419c14aede5802c4f0ef5204ddbbe0eb7d5263524da38d19f43620d8530 *mksnapshot-v32.2.6-darwin-arm64.zip +9f4e4943df4502943994ffa17525b7b5b9a1d889dbc5aeb28bfc40f9146323ec *mksnapshot-v32.2.6-darwin-x64.zip +6295ad1a4ab3b24ac99ec85d35ebfce3861e58185b94d20077e364e81ad935f8 *mksnapshot-v32.2.6-linux-arm64-x64.zip +ed9e1931165a2ff85c1af9f10b3bf8ba05d2dae31331d1d4f5ff1512078e4411 *mksnapshot-v32.2.6-linux-armv7l-x64.zip +6680c63b11e4638708d88c130474ceb2ee92fc58c62cfcd3bf33dda9fee771c2 *mksnapshot-v32.2.6-linux-x64.zip +5a4c45b755b7bbdcad51345f5eb2935ba988987650f9b8540c7ef04015207a2f *mksnapshot-v32.2.6-mas-arm64.zip +1e6243d6a1b68f327457b9dae244ffaeadc265b07a99d9c36f1abcff31e36856 *mksnapshot-v32.2.6-mas-x64.zip +407099537b17860ce7dcea6ba582a854314c29dc152a6df0abcc77ebb0187b4c *mksnapshot-v32.2.6-win32-arm64-x64.zip +f9107536378e19ae9305386dacf6fae924c7ddaad0cf0a6f49694e1e8af6ded5 *mksnapshot-v32.2.6-win32-ia32.zip +82a142db76a2cc5dfb9a89e4cd721cfebc0f3077d2415d8f3d86bb7411f0fe27 *mksnapshot-v32.2.6-win32-x64.zip diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index 632cc7155e0..9e53ad90832 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,7 +1,7 @@ -92e180624259d082562592bb12548037c6a417069be29e452ec5d158d657b4be node-v20.18.0-darwin-arm64.tar.gz -c02aa7560612a4e2cc359fd89fae7aedde370c06db621f2040a4a9f830a125dc node-v20.18.0-darwin-x64.tar.gz -38bccb35c06ee4edbcd00c77976e3fad1d69d2e57c3c0c363d1700a2a2493278 node-v20.18.0-linux-arm64.tar.gz -9a522daa837d4d32dc700bf9b18dea9e21a229b113a22cfcf38f1f2240bbbc47 node-v20.18.0-linux-armv7l.tar.gz -24a5d58a1d4c2903478f4b7c3cfd2eeb5cea2cae3baee11a4dc6a1fed25fec6c node-v20.18.0-linux-x64.tar.gz -e66327f3d1de938059bedda660de2eff1a508b61d777ff247615a019eb153d46 win-arm64/node.exe -35b7c95a379beb606f5798ed83081690df13190077630b234163c6607aa4cc94 win-x64/node.exe +9e92ce1032455a9cc419fe71e908b27ae477799371b45a0844eedb02279922a4 node-v20.18.1-darwin-arm64.tar.gz +c5497dd17c8875b53712edaf99052f961013cedc203964583fc0cfc0aaf93581 node-v20.18.1-darwin-x64.tar.gz +73cd297378572e0bc9dfc187c5ec8cca8d43aee6a596c10ebea1ed5f9ec682b6 node-v20.18.1-linux-arm64.tar.gz +7b7c3315818e9fe57512737c2380fada14d8717ce88945fb6f7b8baadd3cfb92 node-v20.18.1-linux-armv7l.tar.gz +259e5a8bf2e15ecece65bd2a47153262eda71c0b2c9700d5e703ce4951572784 node-v20.18.1-linux-x64.tar.gz +9a52905b5d22b08b5b34e86b8d5358cd22c03fbd0fcb9dbb37c9bd82a8f18c17 win-arm64/node.exe +06c1dec1b428927d6ff01c8f5882f119ec13b61ac77483760aa7fba215c72cf5 win-x64/node.exe diff --git a/build/lib/policies.js b/build/lib/policies.js index aaa59a95657..1560dc7415d 100644 --- a/build/lib/policies.js +++ b/build/lib/policies.js @@ -157,10 +157,10 @@ class ObjectPolicy extends BasePolicy { super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); } renderADMXElements() { - return [``]; + return [``]; } renderADMLPresentationContents() { - return ``; + return ``; } } class StringEnumPolicy extends BasePolicy { diff --git a/build/lib/policies.ts b/build/lib/policies.ts index c00739f2894..f602c8a0d6e 100644 --- a/build/lib/policies.ts +++ b/build/lib/policies.ts @@ -262,11 +262,11 @@ class ObjectPolicy extends BasePolicy { } protected renderADMXElements(): string[] { - return [``]; + return [``]; } renderADMLPresentationContents() { - return ``; + return ``; } } diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index a4b270551ff..5b95ef12efc 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -39,6 +39,9 @@ "--vscode-button-secondaryForeground", "--vscode-button-secondaryHoverBackground", "--vscode-button-separator", + "--vscode-chart-axis", + "--vscode-chart-guide", + "--vscode-chart-line", "--vscode-charts-blue", "--vscode-charts-foreground", "--vscode-charts-green", @@ -362,7 +365,11 @@ "--vscode-inlineEdit-indicator-border", "--vscode-inlineEdit-indicator-foreground", "--vscode-inlineEdit-modifiedBackground", + "--vscode-inlineEdit-modifiedChangedLineBackground", + "--vscode-inlineEdit-modifiedChangedTextBackground", "--vscode-inlineEdit-originalBackground", + "--vscode-inlineEdit-originalChangedLineBackground", + "--vscode-inlineEdit-originalChangedTextBackground", "--vscode-input-background", "--vscode-input-border", "--vscode-input-foreground", @@ -514,6 +521,7 @@ "--vscode-panelStickyScroll-shadow", "--vscode-panelTitle-activeBorder", "--vscode-panelTitle-activeForeground", + "--vscode-panelTitle-border", "--vscode-panelTitle-inactiveForeground", "--vscode-peekView-border", "--vscode-peekViewEditor-background", @@ -610,6 +618,7 @@ "--vscode-sideBarStickyScroll-border", "--vscode-sideBarStickyScroll-shadow", "--vscode-sideBarTitle-background", + "--vscode-sideBarTitle-border", "--vscode-sideBarTitle-foreground", "--vscode-sideBySideEditor-horizontalBorder", "--vscode-sideBySideEditor-verticalBorder", @@ -769,7 +778,9 @@ "--vscode-testing-iconSkipped-retired", "--vscode-testing-iconUnset", "--vscode-testing-iconUnset-retired", - "--vscode-testing-message-error-decorationForeground", + "--vscode-testing-message-error-badgeBackground", + "--vscode-testing-message-error-badgeBorder", + "--vscode-testing-message-error-badgeForeground", "--vscode-testing-message-error-lineBackground", "--vscode-testing-message-info-decorationForeground", "--vscode-testing-message-info-lineBackground", @@ -782,9 +793,6 @@ "--vscode-testing-uncoveredBorder", "--vscode-testing-uncoveredBranchBackground", "--vscode-testing-uncoveredGutterBackground", - "--vscode-testing-message-error-badgeForeground", - "--vscode-testing-message-error-badgeBackground", - "--vscode-testing-message-error-badgeBorder", "--vscode-textBlockQuote-background", "--vscode-textBlockQuote-border", "--vscode-textCodeBlock-background", @@ -904,4 +912,4 @@ "--widget-color", "--text-link-decoration" ] -} +} \ No newline at end of file diff --git a/cgmanifest.json b/cgmanifest.json index 63e0e74d1f8..832a3f604b7 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -516,11 +516,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "7eebd17fa2c1e48e4534f3a69560388fab2f2c07" + "commitHash": "70178110a8e52d28dc957b6b20da94e455922025" } }, "isOnlyProductionDependency": true, - "version": "20.18.0" + "version": "20.18.1" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "a6a0c4cb6d815bbc9503620c7641d57e14b03868" + "commitHash": "10e0ce069260b2cc984c301d5a7ecb3492f0c2f0" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "32.2.5" + "version": "32.2.6" }, { "component": { diff --git a/extensions/git/package.json b/extensions/git/package.json index 1851ef87038..103caae96f7 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -453,6 +453,12 @@ "category": "Git", "enablement": "!operationInProgress" }, + { + "command": "git.checkoutRef", + "title": "%command.checkoutRef%", + "category": "Git", + "enablement": "!operationInProgress" + }, { "command": "git.checkoutRefDetached", "title": "%command.checkoutRefDetached%", @@ -1470,6 +1476,10 @@ "command": "git.copyCommitMessage", "when": "false" }, + { + "command": "git.checkoutRef", + "when": "false" + }, { "command": "git.checkoutRefDetached", "when": "false" @@ -2028,6 +2038,7 @@ "group": "9_copy@2" } ], + "scm/historyItemRef/context": [], "editor/title": [ { "command": "git.openFile", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 1f6775a2da5..d706b650c8a 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -62,6 +62,7 @@ "command.undoCommit": "Undo Last Commit", "command.checkout": "Checkout to...", "command.checkoutDetached": "Checkout to (Detached)...", + "command.checkoutRef": "Checkout", "command.checkoutRefDetached": "Checkout (Detached)", "command.branch": "Create Branch...", "command.branchFrom": "Create Branch From...", diff --git a/extensions/git/src/actionButton.ts b/extensions/git/src/actionButton.ts index 2fbdaf4f97e..d801fbba05f 100644 --- a/extensions/git/src/actionButton.ts +++ b/extensions/git/src/actionButton.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Command, Disposable, Event, EventEmitter, SourceControlActionButton, Uri, workspace, l10n } from 'vscode'; +import { Command, Disposable, Event, EventEmitter, SourceControlActionButton, Uri, workspace, l10n, LogOutputChannel } from 'vscode'; import { Branch, RefType, Status } from './api/git'; import { OperationKind } from './operation'; import { CommitCommandsCenter } from './postCommitCommands'; @@ -49,6 +49,8 @@ export class ActionButton { return; } + this.logger.trace(`[ActionButton][setState] ${JSON.stringify(state)}`); + this._state = state; this._onDidChange.fire(); } @@ -56,8 +58,9 @@ export class ActionButton { private disposables: Disposable[] = []; constructor( - readonly repository: Repository, - readonly postCommitCommandCenter: CommitCommandsCenter) { + private readonly repository: Repository, + private readonly postCommitCommandCenter: CommitCommandsCenter, + private readonly logger: LogOutputChannel) { this._state = { HEAD: undefined, isCheckoutInProgress: false, @@ -102,7 +105,15 @@ export class ActionButton { } // Commit Changes (enabled) -> Publish Branch -> Sync Changes -> Commit Changes (disabled) - return actionButton ?? this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton() ?? this.getCommitActionButton(); + actionButton = actionButton ?? this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton() ?? this.getCommitActionButton(); + + this.logger.trace(`[ActionButton][getButton] ${JSON.stringify({ + command: actionButton?.command.command, + title: actionButton?.command.title, + enabled: actionButton?.enabled + })}`); + + return actionButton; } private getCommitActionButton(): SourceControlActionButton | undefined { diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 9c6bc5ae761..fbc5aaa417b 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -160,6 +160,7 @@ export class GitBlameController { this._model.onDidOpenRepository(this._onDidOpenRepository, this, this._disposables); this._model.onDidCloseRepository(this._onDidCloseRepository, this, this._disposables); + window.onDidChangeActiveTextEditor(e => this._updateTextEditorBlameInformation(e), this, this._disposables); window.onDidChangeTextEditorSelection(e => this._updateTextEditorBlameInformation(e.textEditor), this, this._disposables); window.onDidChangeTextEditorDiffInformation(e => this._updateTextEditorBlameInformation(e.textEditor), this, this._disposables); diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index cc85c83728a..5266955198f 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,7 +5,7 @@ import * as os from 'os'; import * as path from 'path'; -import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, SourceControlHistoryItemRef } from 'vscode'; +import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import { ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote } from './api/git'; @@ -2505,8 +2505,18 @@ export class CommandCenter { return this._checkout(repository, { detached: true, treeish }); } + @command('git.checkoutRef', { repository: true }) + async checkoutRef(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise { + const historyItemRef = historyItem?.references?.find(r => r.id === historyItemRefId); + if (!historyItemRef) { + return false; + } + + return this._checkout(repository, { treeish: historyItemRefId }); + } + @command('git.checkoutRefDetached', { repository: true }) - async checkoutRefDetached(repository: Repository, historyItem?: SourceControlHistoryItemRef): Promise { + async checkoutRefDetached(repository: Repository, historyItem?: SourceControlHistoryItem): Promise { if (!historyItem) { return false; } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 58ebba555c2..1c2e6ca45c2 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -984,7 +984,7 @@ export class Repository implements Disposable { this.commitCommandCenter = new CommitCommandsCenter(globalState, this, postCommitCommandsProviderRegistry); this.disposables.push(this.commitCommandCenter); - const actionButton = new ActionButton(this, this.commitCommandCenter); + const actionButton = new ActionButton(this, this.commitCommandCenter, this.logger); this.disposables.push(actionButton); actionButton.onDidChange(() => this._sourceControl.actionButton = actionButton.button); this._sourceControl.actionButton = actionButton.button; diff --git a/extensions/simple-browser/esbuild-preview.js b/extensions/simple-browser/esbuild-preview.js index cf6e99ca6cb..9c94a67d56f 100644 --- a/extensions/simple-browser/esbuild-preview.js +++ b/extensions/simple-browser/esbuild-preview.js @@ -11,7 +11,7 @@ const outDir = path.join(__dirname, 'media'); require('../esbuild-webview-common').run({ entryPoints: { 'index': path.join(srcDir, 'index.ts'), - 'codicon': path.join(__dirname, 'node_modules', 'vscode-codicons', 'dist', 'codicon.css'), + 'codicon': path.join(__dirname, 'node_modules', '@vscode', 'codicons', 'dist', 'codicon.css'), }, srcDir, outdir: outDir, diff --git a/extensions/simple-browser/package-lock.json b/extensions/simple-browser/package-lock.json index b5d97e00873..d3542946e63 100644 --- a/extensions/simple-browser/package-lock.json +++ b/extensions/simple-browser/package-lock.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@types/vscode-webview": "^1.57.0", - "vscode-codicons": "^0.0.14" + "@vscode/codicons": "^0.0.36" }, "engines": { "vscode": "^1.70.0" @@ -139,6 +139,13 @@ "integrity": "sha512-x3Cb/SMa1IwRHfSvKaZDZOTh4cNoG505c3NjTqGlMC082m++x/ETUmtYniDsw6SSmYzZXO8KBNhYxR0+VqymqA==", "dev": true }, + "node_modules/@vscode/codicons": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.36.tgz", + "integrity": "sha512-wsNOvNMMJ2BY8rC2N2MNBG7yOowV3ov8KlvUE/AiVUlHKTfWsw3OgAOQduX7h0Un6GssKD3aoTVH+TF3DSQwKQ==", + "dev": true, + "license": "CC-BY-4.0" + }, "node_modules/@vscode/extension-telemetry": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz", @@ -151,13 +158,6 @@ "engines": { "vscode": "^1.75.0" } - }, - "node_modules/vscode-codicons": { - "version": "0.0.14", - "resolved": "https://registry.npmjs.org/vscode-codicons/-/vscode-codicons-0.0.14.tgz", - "integrity": "sha512-6CEH5KT9ct5WMw7n5dlX7rB8ya4CUI2FSq1Wk36XaW+c5RglFtAanUV0T+gvZVVFhl/WxfjTvFHq06Hz9c1SLA==", - "deprecated": "This package is deprecated, please use @vscode/codicons https://www.npmjs.com/package/@vscode/codicons", - "dev": true } } } diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 982333b33ef..0d812db6078 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -70,7 +70,7 @@ }, "devDependencies": { "@types/vscode-webview": "^1.57.0", - "vscode-codicons": "^0.0.14" + "@vscode/codicons": "^0.0.36" }, "repository": { "type": "git", diff --git a/extensions/terminal-suggest/src/completions/cd.ts b/extensions/terminal-suggest/src/completions/cd.ts index 89fc633911b..0eb7aaba25b 100644 --- a/extensions/terminal-suggest/src/completions/cd.ts +++ b/extensions/terminal-suggest/src/completions/cd.ts @@ -9,7 +9,6 @@ const cdSpec: Fig.Spec = { args: { name: 'folder', template: 'folders', - isVariadic: true, suggestions: [ { diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 3e681921beb..d4a96ae5ba9 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -232,6 +232,7 @@ export async function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalCon let filesRequested = false; let foldersRequested = false; let specificSuggestionsProvided = false; + const firstCommand = getFirstCommand(terminalContext.commandLine); for (const spec of specs) { const specLabels = getLabel(spec); if (!specLabels) { @@ -245,8 +246,8 @@ export async function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalCon if ( // If the prompt is empty !terminalContext.commandLine - // or the prefix matches the command and the prefix is not equal to the command - || !!prefix && specLabel.startsWith(prefix) + // or the first command matches the command + || !!firstCommand && specLabel.startsWith(firstCommand) ) { // push it to the completion items items.push(createCompletionItem(terminalContext.commandLine, terminalContext.cursorPosition, prefix, specLabel)); @@ -308,10 +309,10 @@ export async function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalCon } } - if (!specificSuggestionsProvided) { + if (!specificSuggestionsProvided && (filesRequested === foldersRequested)) { // Include builitin/available commands in the results for (const command of availableCommands) { - if ((!terminalContext.commandLine.trim() || !!prefix) && command.startsWith(prefix) && !items.find(item => item.label === command)) { + if ((!terminalContext.commandLine.trim() || firstCommand && command.startsWith(firstCommand)) && !items.find(item => item.label === command)) { items.push(createCompletionItem(terminalContext.commandLine, terminalContext.cursorPosition, prefix, command)); } } @@ -394,3 +395,13 @@ function osIsWindows(): boolean { return os.platform() === 'win32'; } +function getFirstCommand(commandLine: string): string | undefined { + const wordsOnLine = commandLine.split(' '); + let firstCommand: string | undefined = wordsOnLine[0]; + if (wordsOnLine.length > 1) { + firstCommand = undefined; + } else if (wordsOnLine.length === 0) { + firstCommand = commandLine; + } + return firstCommand; +} diff --git a/extensions/theme-seti/cgmanifest.json b/extensions/theme-seti/cgmanifest.json index 26ada7ea17a..6d2b5fb51c7 100644 --- a/extensions/theme-seti/cgmanifest.json +++ b/extensions/theme-seti/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "seti-ui", "repositoryUrl": "https://github.com/jesseweed/seti-ui", - "commitHash": "88bcd4a8fca14aa33dd9d50781ad8af3b03c6b90" + "commitHash": "1cac4f30f93cc898103c62dde41823a09b0d7b74" } }, "version": "0.1.0" diff --git a/extensions/theme-seti/icons/seti.woff b/extensions/theme-seti/icons/seti.woff index d7a0f3639c7..edc69c7cf89 100644 Binary files a/extensions/theme-seti/icons/seti.woff and b/extensions/theme-seti/icons/seti.woff differ diff --git a/extensions/theme-seti/icons/vs-seti-icon-theme.json b/extensions/theme-seti/icons/vs-seti-icon-theme.json index 0a51f2aef8c..444f22780b4 100644 --- a/extensions/theme-seti/icons/vs-seti-icon-theme.json +++ b/extensions/theme-seti/icons/vs-seti-icon-theme.json @@ -1433,116 +1433,124 @@ "fontCharacter": "\\E09B", "fontColor": "#f55385" }, - "_vue_light": { + "_vite_light": { "fontCharacter": "\\E09C", + "fontColor": "#b7b73b" + }, + "_vite": { + "fontCharacter": "\\E09C", + "fontColor": "#cbcb41" + }, + "_vue_light": { + "fontCharacter": "\\E09D", "fontColor": "#7fae42" }, "_vue": { - "fontCharacter": "\\E09C", + "fontCharacter": "\\E09D", "fontColor": "#8dc149" }, "_wasm_light": { - "fontCharacter": "\\E09D", + "fontCharacter": "\\E09E", "fontColor": "#9068b0" }, "_wasm": { - "fontCharacter": "\\E09D", + "fontCharacter": "\\E09E", "fontColor": "#a074c4" }, "_wat_light": { - "fontCharacter": "\\E09E", + "fontCharacter": "\\E09F", "fontColor": "#9068b0" }, "_wat": { - "fontCharacter": "\\E09E", + "fontCharacter": "\\E09F", "fontColor": "#a074c4" }, "_webpack_light": { - "fontCharacter": "\\E09F", + "fontCharacter": "\\E0A0", "fontColor": "#498ba7" }, "_webpack": { - "fontCharacter": "\\E09F", + "fontCharacter": "\\E0A0", "fontColor": "#519aba" }, "_wgt_light": { - "fontCharacter": "\\E0A0", + "fontCharacter": "\\E0A1", "fontColor": "#498ba7" }, "_wgt": { - "fontCharacter": "\\E0A0", + "fontCharacter": "\\E0A1", "fontColor": "#519aba" }, "_windows_light": { - "fontCharacter": "\\E0A1", + "fontCharacter": "\\E0A2", "fontColor": "#498ba7" }, "_windows": { - "fontCharacter": "\\E0A1", + "fontCharacter": "\\E0A2", "fontColor": "#519aba" }, "_word_light": { - "fontCharacter": "\\E0A2", + "fontCharacter": "\\E0A3", "fontColor": "#498ba7" }, "_word": { - "fontCharacter": "\\E0A2", + "fontCharacter": "\\E0A3", "fontColor": "#519aba" }, "_xls_light": { - "fontCharacter": "\\E0A3", + "fontCharacter": "\\E0A4", "fontColor": "#7fae42" }, "_xls": { - "fontCharacter": "\\E0A3", + "fontCharacter": "\\E0A4", "fontColor": "#8dc149" }, "_xml_light": { - "fontCharacter": "\\E0A4", + "fontCharacter": "\\E0A5", "fontColor": "#cc6d2e" }, "_xml": { - "fontCharacter": "\\E0A4", + "fontCharacter": "\\E0A5", "fontColor": "#e37933" }, "_yarn_light": { - "fontCharacter": "\\E0A5", + "fontCharacter": "\\E0A6", "fontColor": "#498ba7" }, "_yarn": { - "fontCharacter": "\\E0A5", + "fontCharacter": "\\E0A6", "fontColor": "#519aba" }, "_yml_light": { - "fontCharacter": "\\E0A6", + "fontCharacter": "\\E0A7", "fontColor": "#9068b0" }, "_yml": { - "fontCharacter": "\\E0A6", + "fontCharacter": "\\E0A7", "fontColor": "#a074c4" }, "_zig_light": { - "fontCharacter": "\\E0A7", + "fontCharacter": "\\E0A8", "fontColor": "#cc6d2e" }, "_zig": { - "fontCharacter": "\\E0A7", + "fontCharacter": "\\E0A8", "fontColor": "#e37933" }, "_zip_light": { - "fontCharacter": "\\E0A8", + "fontCharacter": "\\E0A9", "fontColor": "#b8383d" }, "_zip": { - "fontCharacter": "\\E0A8", + "fontCharacter": "\\E0A9", "fontColor": "#cc3e44" }, "_zip_1_light": { - "fontCharacter": "\\E0A8", + "fontCharacter": "\\E0A9", "fontColor": "#627379" }, "_zip_1": { - "fontCharacter": "\\E0A8", + "fontCharacter": "\\E0A9", "fontColor": "#6d8086" } }, @@ -1809,6 +1817,12 @@ "mvnw": "_maven", "pom.xml": "_maven", "tsconfig.json": "_tsconfig", + "vite.config.js": "_vite", + "vite.config.ts": "_vite", + "vite.config.mjs": "_vite", + "vite.config.mts": "_vite", + "vite.config.cjs": "_vite", + "vite.config.cts": "_vite", "swagger.json": "_json_1", "swagger.yml": "_json_1", "swagger.yaml": "_json_1", @@ -2308,6 +2322,12 @@ "mvnw": "_maven_light", "pom.xml": "_maven_light", "tsconfig.json": "_tsconfig_light", + "vite.config.js": "_vite_light", + "vite.config.ts": "_vite_light", + "vite.config.mjs": "_vite_light", + "vite.config.mts": "_vite_light", + "vite.config.cjs": "_vite_light", + "vite.config.cts": "_vite_light", "swagger.json": "_json_1_light", "swagger.yml": "_json_1_light", "swagger.yaml": "_json_1_light", @@ -2382,5 +2402,5 @@ "npm-debug.log": "_npm_ignored_light" } }, - "version": "https://github.com/jesseweed/seti-ui/commit/88bcd4a8fca14aa33dd9d50781ad8af3b03c6b90" + "version": "https://github.com/jesseweed/seti-ui/commit/1cac4f30f93cc898103c62dde41823a09b0d7b74" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f1ee69e00fa..1a665f2f9b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", - "@vscode/proxy-agent": "^0.25.0", + "@vscode/proxy-agent": "^0.26.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", @@ -47,7 +47,6 @@ "node-pty": "^1.1.0-beta22", "open": "^8.4.2", "tas-client-umd": "0.2.0", - "undici": "^6.20.1", "v8-inspect-profiler": "^0.1.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", @@ -96,7 +95,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "32.2.5", + "electron": "32.2.6", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -2561,9 +2560,9 @@ } }, "node_modules/@vscode/proxy-agent": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.25.0.tgz", - "integrity": "sha512-auV9HeIVczu5AGs0goqQZX0LCreRaDGYIltQ2BUxiAWVPuHzqa7ha2Z/4ERz64Z32Xy3uDN9OBzpn5Wm9MWxcQ==", + "version": "0.26.0", + "resolved": "git+ssh://git@github.com/microsoft/vscode-proxy-agent.git#acc515f76b9a591aad38da7fe0d654df98850225", + "integrity": "sha512-1mFUCyDo6FNU+ZUucDuWce9f9OSXEJAMmLlz5GwQYbg2cT6Pr53+wIP4fbBB6AEjzFY10Qjww3Po48+pbof8iQ==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", @@ -2571,7 +2570,8 @@ "debug": "^4.3.4", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", - "socks-proxy-agent": "^8.0.1" + "socks-proxy-agent": "^8.0.1", + "undici": "^6.20.1" }, "optionalDependencies": { "@vscode/windows-ca-certs": "^0.3.1" @@ -5815,9 +5815,9 @@ "dev": true }, "node_modules/electron": { - "version": "32.2.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-32.2.5.tgz", - "integrity": "sha512-8t5IGOvms/JTcLNnlOH7rFCAJIZJAazwYrF7kQsKQSLzDHh4z8mGFrU11NN3W4bIT6Yg5DJNniSWz3O5wJLmCw==", + "version": "32.2.6", + "resolved": "https://registry.npmjs.org/electron/-/electron-32.2.6.tgz", + "integrity": "sha512-aGG1MLvWCf+ECUFBCmaCF52F8312OPAJfph2D0FSsFmlbfnJuNevZCbty2lFzsiIMtU7/QRo6d0ksbgR4s7y3w==", "dev": true, "hasInstallScript": true, "license": "MIT", diff --git a/package.json b/package.json index 64f0e91044e..cc26a8b2ecb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.96.0", - "distro": "9f7044d3679195e9328d2e34fb2d8e833127239b", + "distro": "eee4ba8554f216a6fb025cf4705b8da2c6d21a49", "author": { "name": "Microsoft Corporation" }, @@ -75,7 +75,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.8", - "@vscode/proxy-agent": "^0.25.0", + "@vscode/proxy-agent": "^0.26.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", @@ -105,7 +105,6 @@ "node-pty": "^1.1.0-beta22", "open": "^8.4.2", "tas-client-umd": "0.2.0", - "undici": "^6.20.1", "v8-inspect-profiler": "^0.1.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", @@ -154,7 +153,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "32.2.5", + "electron": "32.2.6", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", diff --git a/remote/.npmrc b/remote/.npmrc index 929e26e4c4b..b74c59d2b8f 100644 --- a/remote/.npmrc +++ b/remote/.npmrc @@ -1,6 +1,6 @@ disturl="https://nodejs.org/dist" -target="20.18.0" -ms_build_id="300025" +target="20.18.1" +ms_build_id="307485" runtime="node" build_from_source="true" legacy-peer-deps="true" diff --git a/remote/package-lock.json b/remote/package-lock.json index b9b7ae801f5..8ad3cea803b 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -13,7 +13,7 @@ "@parcel/watcher": "2.1.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.25.0", + "@vscode/proxy-agent": "^0.26.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.4", @@ -38,7 +38,6 @@ "native-watchdog": "^1.4.1", "node-pty": "^1.1.0-beta22", "tas-client-umd": "0.2.0", - "undici": "^6.20.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.1.0", @@ -131,9 +130,9 @@ "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==" }, "node_modules/@vscode/proxy-agent": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.25.0.tgz", - "integrity": "sha512-auV9HeIVczu5AGs0goqQZX0LCreRaDGYIltQ2BUxiAWVPuHzqa7ha2Z/4ERz64Z32Xy3uDN9OBzpn5Wm9MWxcQ==", + "version": "0.26.0", + "resolved": "git+ssh://git@github.com/microsoft/vscode-proxy-agent.git#acc515f76b9a591aad38da7fe0d654df98850225", + "integrity": "sha512-1mFUCyDo6FNU+ZUucDuWce9f9OSXEJAMmLlz5GwQYbg2cT6Pr53+wIP4fbBB6AEjzFY10Qjww3Po48+pbof8iQ==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", @@ -141,7 +140,8 @@ "debug": "^4.3.4", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", - "socks-proxy-agent": "^8.0.1" + "socks-proxy-agent": "^8.0.1", + "undici": "^6.20.1" }, "optionalDependencies": { "@vscode/windows-ca-certs": "^0.3.1" diff --git a/remote/package.json b/remote/package.json index ff902a43eee..4903025e57c 100644 --- a/remote/package.json +++ b/remote/package.json @@ -8,7 +8,7 @@ "@parcel/watcher": "2.1.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.25.0", + "@vscode/proxy-agent": "^0.26.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.0.4", @@ -33,7 +33,6 @@ "native-watchdog": "^1.4.1", "node-pty": "^1.1.0-beta22", "tas-client-umd": "0.2.0", - "undici": "^6.20.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.1.0", diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 5c832677ca6..b59cceada07 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -52,6 +52,11 @@ echo ### Colorize tests call npm run test-extension -- -l vscode-colorize-tests if %errorlevel% neq 0 exit /b %errorlevel% +echo. +echo ### Terminal Suggest tests +call npm run test-extension -- -l terminal-suggest --enable-proposed-api=vscode.vscode-api-tests +if %errorlevel% neq 0 exit /b %errorlevel% + echo. echo ### TypeScript tests call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\typescript-language-features\test-workspace --extensionDevelopmentPath=%~dp0\..\extensions\typescript-language-features --extensionTestsPath=%~dp0\..\extensions\typescript-language-features\out\test\unit %API_TESTS_EXTRA_ARGS% diff --git a/src/main.ts b/src/main.ts index ff9a5e89296..2d9c977a3bd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -294,8 +294,9 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { // Blink features to configure. // `FontMatchingCTMigration` - Siwtch font matching on macOS to Appkit (Refs https://github.com/microsoft/vscode/issues/224496#issuecomment-2270418470). + // `StandardizedBrowserZoom` - Disable zoom adjustment for bounding box (https://github.com/microsoft/vscode/issues/232750#issuecomment-2459495394) const blinkFeaturesToDisable = - `FontMatchingCTMigration,${app.commandLine.getSwitchValue('disable-blink-features')}`; + `FontMatchingCTMigration,StandardizedBrowserZoom,${app.commandLine.getSwitchValue('disable-blink-features')}`; app.commandLine.appendSwitch('disable-blink-features', blinkFeaturesToDisable); // Support JS Flags diff --git a/src/typings/editContext.d.ts b/src/typings/editContext.d.ts index 5b5da0ac7e9..09585848667 100644 --- a/src/typings/editContext.d.ts +++ b/src/typings/editContext.d.ts @@ -58,8 +58,8 @@ interface EditContextEventHandlersEventMap { type EventHandler = (event: TEvent) => void; -interface TextUpdateEvent extends Event { - new(type: DOMString, options?: TextUpdateEventInit): TextUpdateEvent; +declare class TextUpdateEvent extends Event { + constructor(type: DOMString, options?: TextUpdateEventInit); readonly updateRangeStart: number; readonly updateRangeEnd: number; diff --git a/src/vs/base/browser/fastDomNode.ts b/src/vs/base/browser/fastDomNode.ts index 1ef17e48cd5..9d58aaba9c3 100644 --- a/src/vs/base/browser/fastDomNode.ts +++ b/src/vs/base/browser/fastDomNode.ts @@ -39,6 +39,10 @@ export class FastDomNode { public readonly domNode: T ) { } + public focus(): void { + this.domNode.focus(); + } + public setMaxWidth(_maxWidth: number | string): void { const maxWidth = numberAsPixels(_maxWidth); if (this._maxWidth === maxWidth) { diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index a9b9358eca5..2b1c64c144f 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/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index be65784fd80..64efdcaabea 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -1177,7 +1177,7 @@ export class FindController extends AbstractFindController !FuzzyScore.isDefault(node.filterData as any as FuzzyScore)); + this.tree.focusNext(0, true, undefined, (node) => this.shouldAllowFocus(node)); } const focus = this.tree.getFocus(); diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 418c47a4754..9d0d2cbf4b2 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -227,8 +227,10 @@ export interface IAsyncFindToggles { findMode: TreeFindMode; } -export interface IAsyncFindResultMetadata { +export interface IAsyncFindResult { warningMessage?: string; + matchCount: number; + isMatch(element: T): boolean; } export interface IAsyncFindProvider { @@ -241,7 +243,7 @@ export interface IAsyncFindProvider { /** * `find` is called when the user types one or more character into the find input. */ - find(pattern: string, toggles: IAsyncFindToggles, token: CancellationToken): Promise; + find(pattern: string, toggles: IAsyncFindToggles, token: CancellationToken): Promise | undefined>; /** * `isVisible` is called to check if an element should be visible. @@ -285,8 +287,10 @@ class AsyncFindFilter extends FindFilter { } +// TODO Fix types class AsyncFindController extends FindController { private activeTokenSource: CancellationTokenSource | undefined; + private activeFindMetadata: IAsyncFindResult | undefined; private activeSession = false; private asyncWorkInProgress = false; private taskQueue = new ThrottledDelayer(250); @@ -342,13 +346,15 @@ class AsyncFindController extends FindController extends FindController 0; + this.renderMessage(showNotFound); + + if (this.pattern.length) { + this.alertResults(this.activeFindMetadata.matchCount); } } @@ -385,6 +398,23 @@ class AsyncFindController extends FindController): boolean { + return this.shouldFocusWhenNavigating(node as ITreeNode | null, TFilterData>); + } + + shouldFocusWhenNavigating(node: ITreeNode | null, TFilterData>): boolean { + if (!this.activeSession || !this.activeFindMetadata) { + return true; + } + + const element = node.element?.element as T | undefined; + if (element && this.activeFindMetadata.isMatch(element)) { + return true; + } + + return !FuzzyScore.isDefault(node.filterData as any as FuzzyScore); + } } function asObjectTreeOptions(options?: IAsyncDataTreeOptions): IObjectTreeOptions, TFilterData> | undefined { @@ -534,6 +564,8 @@ export class AsyncDataTree implements IDisposable get onDidUpdateOptions(): Event { return this.tree.onDidUpdateOptions; } + private focusNavigationFilter: ((node: ITreeNode | null, TFilterData>) => boolean) | undefined; + readonly onDidChangeFindOpenState: Event; get onDidChangeStickyScrollFocused(): Event { return this.tree.onDidChangeStickyScrollFocused; } @@ -600,6 +632,7 @@ export class AsyncDataTree implements IDisposable const findOptions = { styles: options.findWidgetStyles, showNotFoundMessage: options.showNotFoundMessage }; this.findController = this.disposables.add(new AsyncFindController(this.tree, options.findProvider!, findFilter!, this.tree.options.contextViewProvider!, findOptions)); + this.focusNavigationFilter = node => this.findController!.shouldFocusWhenNavigating(node); this.onDidChangeFindOpenState = this.findController!.onDidChangeOpenState; this.onDidChangeFindMode = this.findController!.onDidChangeMode; this.onDidChangeFindMatchType = this.findController!.onDidChangeMatchType; @@ -935,27 +968,27 @@ export class AsyncDataTree implements IDisposable } focusNext(n = 1, loop = false, browserEvent?: UIEvent): void { - this.tree.focusNext(n, loop, browserEvent); + this.tree.focusNext(n, loop, browserEvent, this.focusNavigationFilter); } focusPrevious(n = 1, loop = false, browserEvent?: UIEvent): void { - this.tree.focusPrevious(n, loop, browserEvent); + this.tree.focusPrevious(n, loop, browserEvent, this.focusNavigationFilter); } focusNextPage(browserEvent?: UIEvent): Promise { - return this.tree.focusNextPage(browserEvent); + return this.tree.focusNextPage(browserEvent, this.focusNavigationFilter); } focusPreviousPage(browserEvent?: UIEvent): Promise { - return this.tree.focusPreviousPage(browserEvent); + return this.tree.focusPreviousPage(browserEvent, this.focusNavigationFilter); } focusLast(browserEvent?: UIEvent): void { - this.tree.focusLast(browserEvent); + this.tree.focusLast(browserEvent, this.focusNavigationFilter); } focusFirst(browserEvent?: UIEvent): void { - this.tree.focusFirst(browserEvent); + this.tree.focusFirst(browserEvent, this.focusNavigationFilter); } getFocus(): T[] { diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index 3bacd8c545e..f7291fbe09b 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -582,4 +582,5 @@ export const codiconsLibrary = { goToEditingSession: register('go-to-editing-session', 0xec35), editSession: register('edit-session', 0xec36), codeReview: register('code-review', 0xec37), + copilotWarning: register('copilot-warning', 0xec38), } as const; diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 01eaed143e9..cf574164ad0 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -305,22 +305,15 @@ export interface IAiGeneratedWorkspaceTrust { export interface IDefaultChatAgent { readonly extensionId: string; - readonly name: string; - readonly icon: string; readonly chatExtensionId: string; - readonly chatName: string; - readonly chatWelcomeTitle: string; readonly documentationUrl: string; readonly privacyStatementUrl: string; readonly skusDocumentationUrl: string; + readonly manageSettingsUrl: string; + readonly managePlanUrl: string; readonly providerId: string; readonly providerName: string; readonly providerScopes: string[][]; readonly entitlementUrl: string; - readonly entitlementChatEnabled: string; readonly entitlementSignupLimitedUrl: string; - readonly entitlementCanSignupLimited: string; - readonly entitlementSkuType: string; - readonly entitlementSkuTypeLimited: string; - readonly entitlementSkuTypeLimitedName: string; } diff --git a/src/vs/editor/browser/config/editorConfiguration.ts b/src/vs/editor/browser/config/editorConfiguration.ts index d65d85cfe25..701c940c736 100644 --- a/src/vs/editor/browser/config/editorConfiguration.ts +++ b/src/vs/editor/browser/config/editorConfiguration.ts @@ -22,6 +22,7 @@ import { AccessibilitySupport, IAccessibilityService } from '../../../platform/a import { getWindow, getWindowById } from '../../../base/browser/dom.js'; import { PixelRatio } from '../../../base/browser/pixelRatio.js'; import { MenuId } from '../../../platform/actions/common/actions.js'; +import { InputMode } from '../../common/inputMode.js'; export interface IEditorConstructionOptions extends IEditorOptions { /** @@ -95,6 +96,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat this._register(FontMeasurements.onDidChange(() => this._recomputeOptions())); this._register(PixelRatio.getInstance(getWindow(container)).onDidChange(() => this._recomputeOptions())); this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => this._recomputeOptions())); + this._register(InputMode.onDidChangeInputMode(() => this._recomputeOptions())); } private _recomputeOptions(): void { @@ -126,6 +128,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat emptySelectionClipboard: partialEnv.emptySelectionClipboard, pixelRatio: partialEnv.pixelRatio, tabFocusMode: TabFocus.getTabFocusMode(), + inputMode: InputMode.getInputMode(), accessibilitySupport: partialEnv.accessibilitySupport, glyphMarginDecorationLaneCount: this._glyphMarginDecorationLaneCount }; diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index cab5f079c99..f97b711c44f 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -75,8 +75,6 @@ export class NativeEditContext extends AbstractEditContext { this.textArea = new FastDomNode(document.createElement('textarea')); this.textArea.setClassName('native-edit-context-textarea'); - this._register(NativeEditContextRegistry.registerTextArea(ownerID, this.textArea.domNode)); - this._updateDomAttributes(); overflowGuardContainer.appendChild(this.domNode); @@ -85,7 +83,12 @@ export class NativeEditContext extends AbstractEditContext { this._selectionChangeListener = this._register(new MutableDisposable()); this._focusTracker = this._register(new FocusTracker(this.domNode.domNode, (newFocusValue: boolean) => { - this._selectionChangeListener.value = newFocusValue ? this._setSelectionChangeListener(viewController) : undefined; + if (newFocusValue) { + this._selectionChangeListener.value = this._setSelectionChangeListener(viewController); + this._screenReaderSupport.setIgnoreSelectionChangeTime('onFocus'); + } else { + this._selectionChangeListener.value = undefined; + } this._context.viewModel.setHasFocus(newFocusValue); })); @@ -97,6 +100,9 @@ export class NativeEditContext extends AbstractEditContext { this._register(addDisposableListener(this.domNode.domNode, 'copy', (e) => this._ensureClipboardGetsEditorSelection(e))); this._register(addDisposableListener(this.domNode.domNode, 'cut', (e) => { + // Pretend here we touched the text area, as the `cut` event will most likely + // result in a `selectionchange` event which we want to ignore + this._screenReaderSupport.setIgnoreSelectionChangeTime('onCut'); this._ensureClipboardGetsEditorSelection(e); viewController.cut(); })); @@ -139,6 +145,9 @@ export class NativeEditContext extends AbstractEditContext { this._context.viewModel.onCompositionEnd(); })); this._register(addDisposableListener(this.textArea.domNode, 'paste', (e) => { + // Pretend here we touched the text area, as the `paste` event will most likely + // result in a `selectionchange` event which we want to ignore + this._screenReaderSupport.setIgnoreSelectionChangeTime('onPaste'); e.preventDefault(); if (!e.clipboardData) { return; @@ -160,6 +169,7 @@ export class NativeEditContext extends AbstractEditContext { } viewController.paste(text, pasteOnNewLine, multicursorText, mode); })); + this._register(NativeEditContextRegistry.register(ownerID, this)); } // --- Public methods --- @@ -204,6 +214,10 @@ export class NativeEditContext extends AbstractEditContext { return true; } + public onWillPaste(): void { + this._screenReaderSupport.setIgnoreSelectionChangeTime('onWillPaste'); + } + public writeScreenReaderContent(): void { this._screenReaderSupport.writeScreenReaderContent(); } @@ -455,6 +469,9 @@ export class NativeEditContext extends AbstractEditContext { // When using a Braille display or NVDA for example, it is possible for users to reposition the // system caret. This is reflected in Chrome as a `selectionchange` event and needs to be reflected within the editor. + // `selectionchange` events often come multiple times for a single logical change + // so throttle multiple `selectionchange` events that burst in a short period of time. + let previousSelectionChangeEventTime = 0; return addDisposableListener(this.domNode.domNode.ownerDocument, 'selectionchange', () => { const isScreenReaderOptimized = this._accessibilityService.isScreenReaderOptimized(); if (!this.isFocused() || !isScreenReaderOptimized) { @@ -464,6 +481,21 @@ export class NativeEditContext extends AbstractEditContext { if (!screenReaderContentState) { return; } + const now = Date.now(); + const delta1 = now - previousSelectionChangeEventTime; + previousSelectionChangeEventTime = now; + if (delta1 < 5) { + // received another `selectionchange` event within 5ms of the previous `selectionchange` event + // => ignore it + return; + } + const delta2 = now - this._screenReaderSupport.getIgnoreSelectionChangeTime(); + this._screenReaderSupport.resetSelectionChangeTime(); + if (delta2 < 100) { + // received a `selectionchange` event within 100ms since we touched the textarea + // => ignore it, since we caused it + return; + } const activeDocument = getActiveWindow().document; const activeDocumentSelection = activeDocument.getSelection(); if (!activeDocumentSelection) { @@ -474,11 +506,14 @@ export class NativeEditContext extends AbstractEditContext { return; } const range = activeDocumentSelection.getRangeAt(0); - const model = this._context.viewModel.model; - const offsetOfStartOfScreenReaderContent = model.getOffsetAt(screenReaderContentState.startPositionWithinEditor); + const viewModel = this._context.viewModel; + const model = viewModel.model; + const coordinatesConverter = viewModel.coordinatesConverter; + const modelScreenReaderContentStartPositionWithinEditor = coordinatesConverter.convertViewPositionToModelPosition(screenReaderContentState.startPositionWithinEditor); + const offsetOfStartOfScreenReaderContent = model.getOffsetAt(modelScreenReaderContentStartPositionWithinEditor); let offsetOfSelectionStart = range.startOffset + offsetOfStartOfScreenReaderContent; let offsetOfSelectionEnd = range.endOffset + offsetOfStartOfScreenReaderContent; - const modelUsesCRLF = this._context.viewModel.model.getEndOfLineSequence() === EndOfLineSequence.CRLF; + const modelUsesCRLF = model.getEndOfLineSequence() === EndOfLineSequence.CRLF; if (modelUsesCRLF) { const screenReaderContentText = screenReaderContentState.value; const offsetTransformer = new PositionOffsetTransformer(screenReaderContentText); diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContextRegistry.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContextRegistry.ts index 763c2099fc1..eda35288125 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContextRegistry.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContextRegistry.ts @@ -4,22 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { NativeEditContext } from './nativeEditContext.js'; class NativeEditContextRegistryImpl { - private _textAreaMapping: Map = new Map(); + private _nativeEditContextMapping: Map = new Map(); - registerTextArea(ownerID: string, textArea: HTMLTextAreaElement): IDisposable { - this._textAreaMapping.set(ownerID, textArea); + register(ownerID: string, nativeEditContext: NativeEditContext): IDisposable { + this._nativeEditContextMapping.set(ownerID, nativeEditContext); return { dispose: () => { - this._textAreaMapping.delete(ownerID); + this._nativeEditContextMapping.delete(ownerID); } }; } - getTextArea(ownerID: string): HTMLTextAreaElement | undefined { - return this._textAreaMapping.get(ownerID); + get(ownerID: string): NativeEditContext | undefined { + return this._nativeEditContextMapping.get(ownerID); } } diff --git a/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts b/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts index a668f4e91c6..51479acdd72 100644 --- a/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts +++ b/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts @@ -29,6 +29,7 @@ export class ScreenReaderSupport { private _lineHeight: number = 1; private _fontInfo: FontInfo | undefined; private _accessibilityPageSize: number = 1; + private _ignoreSelectionChangeTime: number = 0; private _primarySelection: Selection = new Selection(1, 1, 1, 1); private _screenReaderContentState: ScreenReaderContentState | undefined; @@ -43,6 +44,18 @@ export class ScreenReaderSupport { this._updateDomAttributes(); } + public setIgnoreSelectionChangeTime(reason: string): void { + this._ignoreSelectionChangeTime = Date.now(); + } + + public getIgnoreSelectionChangeTime(): number { + return this._ignoreSelectionChangeTime; + } + + public resetSelectionChangeTime(): void { + this._ignoreSelectionChangeTime = 0; + } + public onConfigurationChanged(e: ViewConfigurationChangedEvent): void { this._updateConfigurationSettings(); this._updateDomAttributes(); @@ -129,6 +142,7 @@ export class ScreenReaderSupport { return; } if (this._domNode.domNode.textContent !== this._screenReaderContentState.value) { + this.setIgnoreSelectionChangeTime('setValue'); this._domNode.domNode.textContent = this._screenReaderContentState.value; } this._setSelectionOfScreenReaderContent(this._screenReaderContentState.selectionStart, this._screenReaderContentState.selectionEnd); @@ -175,6 +189,7 @@ export class ScreenReaderSupport { const range = new globalThis.Range(); range.setStart(textContent, selectionOffsetStart); range.setEnd(textContent, selectionOffsetEnd); + this.setIgnoreSelectionChangeTime('setRange'); activeDocumentSelection.removeAllRanges(); activeDocumentSelection.addRange(range); } diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts index b69180772cc..b4380373c52 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts @@ -63,7 +63,7 @@ export class ViewCursor { const options = this._context.configuration.options; const fontInfo = options.get(EditorOption.fontInfo); - this._cursorStyle = options.get(EditorOption.cursorStyle); + this._cursorStyle = options.get(EditorOption.effectiveCursorStyle); this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; this._lineCursorWidth = Math.min(options.get(EditorOption.cursorWidth), this._typicalHalfwidthCharacterWidth); @@ -130,7 +130,7 @@ export class ViewCursor { const options = this._context.configuration.options; const fontInfo = options.get(EditorOption.fontInfo); - this._cursorStyle = options.get(EditorOption.cursorStyle); + this._cursorStyle = options.get(EditorOption.effectiveCursorStyle); this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; this._lineCursorWidth = Math.min(options.get(EditorOption.cursorWidth), this._typicalHalfwidthCharacterWidth); diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts index fc8695d3e1f..823a8e5c0a5 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts @@ -59,7 +59,7 @@ export class ViewCursors extends ViewPart { const options = this._context.configuration.options; this._readOnly = options.get(EditorOption.readOnly); this._cursorBlinking = options.get(EditorOption.cursorBlinking); - this._cursorStyle = options.get(EditorOption.cursorStyle); + this._cursorStyle = options.get(EditorOption.effectiveCursorStyle); this._cursorSmoothCaretAnimation = options.get(EditorOption.cursorSmoothCaretAnimation); this._experimentalEditContextEnabled = options.get(EditorOption.experimentalEditContextEnabled); this._selectionIsEmpty = true; @@ -114,7 +114,7 @@ export class ViewCursors extends ViewPart { this._readOnly = options.get(EditorOption.readOnly); this._cursorBlinking = options.get(EditorOption.cursorBlinking); - this._cursorStyle = options.get(EditorOption.cursorStyle); + this._cursorStyle = options.get(EditorOption.effectiveCursorStyle); this._cursorSmoothCaretAnimation = options.get(EditorOption.cursorSmoothCaretAnimation); this._experimentalEditContextEnabled = options.get(EditorOption.experimentalEditContextEnabled); diff --git a/src/vs/editor/common/commands/replaceCommand.ts b/src/vs/editor/common/commands/replaceCommand.ts index f4beabe78a6..779dfd9a7b9 100644 --- a/src/vs/editor/common/commands/replaceCommand.ts +++ b/src/vs/editor/common/commands/replaceCommand.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Position } from '../core/position.js'; import { Range } from '../core/range.js'; import { Selection, SelectionDirection } from '../core/selection.js'; import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from '../editorCommon.js'; @@ -31,6 +32,38 @@ export class ReplaceCommand implements ICommand { } } +export class ReplaceOvertypeCommand implements ICommand { + + private readonly _range: Range; + private readonly _text: string; + public readonly insertsAutoWhitespace: boolean; + + constructor(range: Range, text: string, insertsAutoWhitespace: boolean = false) { + this._range = range; + this._text = text; + this.insertsAutoWhitespace = insertsAutoWhitespace; + } + + public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { + const intialStartPosition = this._range.getStartPosition(); + const initialEndPosition = this._range.getEndPosition(); + const initialEndLineNumber = initialEndPosition.lineNumber; + const offsetDelta = this._text.length + (this._range.isEmpty() ? 0 : -1); + let endPosition = addPositiveOffsetToModelPosition(model, initialEndPosition, offsetDelta); + if (endPosition.lineNumber > initialEndLineNumber) { + endPosition = new Position(initialEndLineNumber, model.getLineMaxColumn(initialEndLineNumber)); + } + const replaceRange = Range.fromPositions(intialStartPosition, endPosition); + builder.addTrackedEditOperation(replaceRange, this._text); + } + + public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { + const inverseEditOperations = helper.getInverseEditOperations(); + const srcRange = inverseEditOperations[0].range; + return Selection.fromPositions(srcRange.getEndPosition()); + } +} + export class ReplaceCommandThatSelectsText implements ICommand { private readonly _range: Range; @@ -102,6 +135,33 @@ export class ReplaceCommandWithOffsetCursorState implements ICommand { } } +export class ReplaceOvertypeCommandOnCompositionEnd implements ICommand { + + private readonly _range: Range; + + constructor(range: Range) { + this._range = range; + } + + public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { + const text = model.getValueInRange(this._range); + const initialEndPosition = this._range.getEndPosition(); + const initialEndLineNumber = initialEndPosition.lineNumber; + let endPosition = addPositiveOffsetToModelPosition(model, initialEndPosition, text.length); + if (endPosition.lineNumber > initialEndLineNumber) { + endPosition = new Position(initialEndLineNumber, model.getLineMaxColumn(initialEndLineNumber)); + } + const replaceRange = Range.fromPositions(initialEndPosition, endPosition); + builder.addTrackedEditOperation(replaceRange, ''); + } + + public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { + const inverseEditOperations = helper.getInverseEditOperations(); + const srcRange = inverseEditOperations[0].range; + return Selection.fromPositions(srcRange.getEndPosition()); + } +} + export class ReplaceCommandThatPreservesSelection implements ICommand { private readonly _range: Range; @@ -127,3 +187,29 @@ export class ReplaceCommandThatPreservesSelection implements ICommand { return helper.getTrackedSelection(this._selectionId!); } } + +function addPositiveOffsetToModelPosition(model: ITextModel, position: Position, offset: number): Position { + if (offset < 0) { + throw new Error('Unexpected negative delta'); + } + const lineCount = model.getLineCount(); + let endPosition = new Position(lineCount, model.getLineMaxColumn(lineCount)); + for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { + if (lineNumber === position.lineNumber) { + const futureOffset = offset - model.getLineMaxColumn(position.lineNumber) + position.column; + if (futureOffset <= 0) { + endPosition = new Position(position.lineNumber, position.column + offset); + break; + } + offset = futureOffset; + } else { + const futureOffset = offset - model.getLineMaxColumn(lineNumber); + if (futureOffset <= 0) { + endPosition = new Position(lineNumber, offset); + break; + } + offset = futureOffset; + } + } + return endPosition; +} diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 17e0805cbdb..1f77b1348b5 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -239,10 +239,19 @@ export interface IEditorOptions { */ cursorSmoothCaretAnimation?: 'off' | 'explicit' | 'on'; /** - * Control the cursor style, either 'block' or 'line'. + * Control the cursor style in insert mode. * Defaults to 'line'. */ cursorStyle?: 'line' | 'block' | 'underline' | 'line-thin' | 'block-outline' | 'underline-thin'; + /** + * Control the cursor style in overtype mode. + * Defaults to 'block'. + */ + overtypeCursorStyle?: 'line' | 'block' | 'underline' | 'line-thin' | 'block-outline' | 'underline-thin'; + /** + * Controls whether paste in overtype mode should overwrite or insert. + */ + overtypeOnPaste?: boolean; /** * Control the width of the cursor when cursorStyle is set to 'line' */ @@ -974,6 +983,7 @@ export interface IEnvironmentalOptions { readonly emptySelectionClipboard: boolean; readonly pixelRatio: number; readonly tabFocusMode: boolean; + readonly inputMode: 'insert' | 'overtype'; readonly accessibilitySupport: AccessibilitySupport; readonly glyphMarginDecorationLaneCount: number; } @@ -1889,6 +1899,23 @@ class EditorFontInfo extends ComputedEditorOption { + + constructor() { + super(EditorOption.effectiveCursorStyle); + } + + public compute(env: IEnvironmentalOptions, options: IComputedEditorOptions, _: TextEditorCursorStyle): TextEditorCursorStyle { + return env.inputMode === 'overtype' ? + options.get(EditorOption.overtypeCursorStyle) : + options.get(EditorOption.cursorStyle); + } +} + +//#endregion + //#region fontSize class EditorFontSize extends SimpleEditorOption { @@ -4170,6 +4197,7 @@ export interface IInlineSuggestOptions { enabled?: boolean; useMixedLinesDiff?: 'never' | 'whenPossible' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; + onlyShowWhenCloseToCursor?: boolean; }; }; } @@ -4201,6 +4229,7 @@ class InlineEditorSuggest extends BaseEditorOption new ReplaceOvertypeCommandOnCompositionEnd(composition.insertedTextRange)); + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: false + }); + } +} + export class SurroundSelectionOperation { public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult | undefined { @@ -483,11 +499,12 @@ export class InterceptorElectricCharOperation { export class SimpleCharacterTypeOperation { - public static getEdits(prevEditOperationType: EditOperationType, selections: Selection[], ch: string): EditOperationResult { + public static getEdits(config: CursorConfiguration, prevEditOperationType: EditOperationType, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult { // A simple character type const commands: ICommand[] = []; for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = new ReplaceCommand(selections[i], ch); + const ChosenReplaceCommand = config.inputMode === 'overtype' && !isDoingComposition ? ReplaceOvertypeCommand : ReplaceCommand; + commands[i] = new ChosenReplaceCommand(selections[i], ch); } const opType = getTypingOperation(ch, prevEditOperationType); @@ -677,7 +694,9 @@ export class PasteOperation { private static _distributedPaste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string[]): EditOperationResult { const commands: ICommand[] = []; for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = new ReplaceCommand(selections[i], text[i]); + const shouldOvertypeOnPaste = config.overtypeOnPaste && config.inputMode === 'overtype'; + const ChosenReplaceCommand = shouldOvertypeOnPaste ? ReplaceOvertypeCommand : ReplaceCommand; + commands[i] = new ChosenReplaceCommand(selections[i], text[i]); } return new EditOperationResult(EditOperationType.Other, commands, { shouldPushStackElementBefore: true, @@ -701,7 +720,9 @@ export class PasteOperation { const typeSelection = new Range(position.lineNumber, 1, position.lineNumber, 1); commands[i] = new ReplaceCommandThatPreservesSelection(typeSelection, text, selection, true); } else { - commands[i] = new ReplaceCommand(selection, text); + const shouldOvertypeOnPaste = config.overtypeOnPaste && config.inputMode === 'overtype'; + const ChosenReplaceCommand = shouldOvertypeOnPaste ? ReplaceOvertypeCommand : ReplaceCommand; + commands[i] = new ChosenReplaceCommand(selection, text); } } return new EditOperationResult(EditOperationType.Other, commands, { diff --git a/src/vs/editor/common/cursor/cursorTypeOperations.ts b/src/vs/editor/common/cursor/cursorTypeOperations.ts index c518f3f97c4..b6d1a559cd0 100644 --- a/src/vs/editor/common/cursor/cursorTypeOperations.ts +++ b/src/vs/editor/common/cursor/cursorTypeOperations.ts @@ -11,7 +11,7 @@ import { Selection } from '../core/selection.js'; import { Position } from '../core/position.js'; import { ICommand } from '../editorCommon.js'; import { ITextModel } from '../model.js'; -import { AutoClosingOpenCharTypeOperation, AutoClosingOvertypeOperation, AutoClosingOvertypeWithInterceptorsOperation, AutoIndentOperation, CompositionOperation, EnterOperation, InterceptorElectricCharOperation, PasteOperation, shiftIndent, shouldSurroundChar, SimpleCharacterTypeOperation, SurroundSelectionOperation, TabOperation, TypeWithoutInterceptorsOperation, unshiftIndent } from './cursorTypeEditOperations.js'; +import { AutoClosingOpenCharTypeOperation, AutoClosingOvertypeOperation, AutoClosingOvertypeWithInterceptorsOperation, AutoIndentOperation, CompositionOperation, CompositionEndOvertypeOperation, EnterOperation, InterceptorElectricCharOperation, PasteOperation, shiftIndent, shouldSurroundChar, SimpleCharacterTypeOperation, SurroundSelectionOperation, TabOperation, TypeWithoutInterceptorsOperation, unshiftIndent } from './cursorTypeEditOperations.js'; export class TypeOperations { @@ -90,7 +90,7 @@ export class TypeOperations { if (!insertedText || insertedText.length !== 1) { // we're only interested in the case where a single character was inserted - return null; + return CompositionEndOvertypeOperation.getEdits(config, compositions); } const ch = insertedText; @@ -159,7 +159,7 @@ export class TypeOperations { return autoClosingOpenCharEdits; } - return null; + return CompositionEndOvertypeOperation.getEdits(config, compositions); } public static typeWithInterceptors(isDoingComposition: boolean, prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult { @@ -194,7 +194,7 @@ export class TypeOperations { return interceptorElectricCharOperation; } - return SimpleCharacterTypeOperation.getEdits(prevEditOperationType, selections, ch); + return SimpleCharacterTypeOperation.getEdits(config, prevEditOperationType, selections, ch, isDoingComposition); } public static typeWithoutInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], str: string): EditOperationResult { @@ -210,5 +210,6 @@ export class CompositionOutcome { public readonly insertedText: string, public readonly insertedSelectionStart: number, public readonly insertedSelectionEnd: number, + public readonly insertedTextRange: Range, ) { } } diff --git a/src/vs/editor/common/cursorCommon.ts b/src/vs/editor/common/cursorCommon.ts index 52f8673a327..2e1c78484fc 100644 --- a/src/vs/editor/common/cursorCommon.ts +++ b/src/vs/editor/common/cursorCommon.ts @@ -17,6 +17,7 @@ import { createScopedLineTokens } from './languages/supports.js'; import { IElectricAction } from './languages/supports/electricCharacter.js'; import { CursorColumns } from './core/cursorColumns.js'; import { normalizeIndentation } from './core/indentation.js'; +import { InputMode } from './inputMode.js'; export interface IColumnSelectData { isReal: boolean; @@ -77,6 +78,7 @@ export class CursorConfiguration { public readonly blockCommentStartToken: string | null; public readonly shouldAutoCloseBefore: { quote: (ch: string) => boolean; bracket: (ch: string) => boolean; comment: (ch: string) => boolean }; public readonly wordSegmenterLocales: string[]; + public readonly overtypeOnPaste: boolean; private readonly _languageId: string; private _electricChars: { [key: string]: boolean } | null; @@ -99,6 +101,7 @@ export class CursorConfiguration { || e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.readOnly) || e.hasChanged(EditorOption.wordSegmenterLocales) + || e.hasChanged(EditorOption.overtypeOnPaste) ); } @@ -137,6 +140,7 @@ export class CursorConfiguration { this.autoSurround = options.get(EditorOption.autoSurround); this.autoIndent = options.get(EditorOption.autoIndent); this.wordSegmenterLocales = options.get(EditorOption.wordSegmenterLocales); + this.overtypeOnPaste = options.get(EditorOption.overtypeOnPaste); this.surroundingPairs = {}; this._electricChars = null; @@ -173,6 +177,10 @@ export class CursorConfiguration { return this._electricChars; } + public get inputMode(): 'insert' | 'overtype' { + return InputMode.getInputMode(); + } + /** * Should return opening bracket type to match indentation with */ diff --git a/src/vs/editor/common/inputMode.ts b/src/vs/editor/common/inputMode.ts new file mode 100644 index 00000000000..304ac1f140e --- /dev/null +++ b/src/vs/editor/common/inputMode.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../base/common/event.js'; + +class InputModeImpl { + + private _inputMode: 'overtype' | 'insert' = 'insert'; + private readonly _onDidChangeInputMode = new Emitter<'overtype' | 'insert'>(); + public readonly onDidChangeInputMode: Event<'overtype' | 'insert'> = this._onDidChangeInputMode.event; + + public getInputMode(): 'overtype' | 'insert' { + return this._inputMode; + } + + public setInputMode(inputMode: 'overtype' | 'insert'): void { + this._inputMode = inputMode; + this._onDidChangeInputMode.fire(this._inputMode); + } +} + +/** + * Controls the type mode, whether insert or overtype + */ +export const InputMode = new InputModeImpl(); diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 08c08a57196..889a89a5d68 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -903,7 +903,7 @@ export interface DocumentPasteEdit { readonly title: string; readonly kind: HierarchicalKind; readonly handledMimeType?: string; - readonly yieldTo?: readonly DropYieldTo[]; + yieldTo?: readonly DropYieldTo[]; insertText: string | { readonly snippet: string }; additionalEdit?: WorkspaceEdit; } diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 01607d8310b..db9a28f4fed 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -257,75 +257,78 @@ export enum EditorOption { multiCursorLimit = 81, occurrencesHighlight = 82, occurrencesHighlightDelay = 83, - overviewRulerBorder = 84, - overviewRulerLanes = 85, - padding = 86, - pasteAs = 87, - parameterHints = 88, - peekWidgetDefaultFocus = 89, - placeholder = 90, - definitionLinkOpensInPeek = 91, - quickSuggestions = 92, - quickSuggestionsDelay = 93, - readOnly = 94, - readOnlyMessage = 95, - renameOnType = 96, - renderControlCharacters = 97, - renderFinalNewline = 98, - renderLineHighlight = 99, - renderLineHighlightOnlyWhenFocus = 100, - renderValidationDecorations = 101, - renderWhitespace = 102, - revealHorizontalRightPadding = 103, - roundedSelection = 104, - rulers = 105, - scrollbar = 106, - scrollBeyondLastColumn = 107, - scrollBeyondLastLine = 108, - scrollPredominantAxis = 109, - selectionClipboard = 110, - selectionHighlight = 111, - selectOnLineNumbers = 112, - showFoldingControls = 113, - showUnused = 114, - snippetSuggestions = 115, - smartSelect = 116, - smoothScrolling = 117, - stickyScroll = 118, - stickyTabStops = 119, - stopRenderingLineAfter = 120, - suggest = 121, - suggestFontSize = 122, - suggestLineHeight = 123, - suggestOnTriggerCharacters = 124, - suggestSelection = 125, - tabCompletion = 126, - tabIndex = 127, - unicodeHighlighting = 128, - unusualLineTerminators = 129, - useShadowDOM = 130, - useTabStops = 131, - wordBreak = 132, - wordSegmenterLocales = 133, - wordSeparators = 134, - wordWrap = 135, - wordWrapBreakAfterCharacters = 136, - wordWrapBreakBeforeCharacters = 137, - wordWrapColumn = 138, - wordWrapOverride1 = 139, - wordWrapOverride2 = 140, - wrappingIndent = 141, - wrappingStrategy = 142, - showDeprecated = 143, - inlayHints = 144, - editorClassName = 145, - pixelRatio = 146, - tabFocusMode = 147, - layoutInfo = 148, - wrappingInfo = 149, - defaultColorDecorators = 150, - colorDecoratorsActivatedOn = 151, - inlineCompletionsAccessibilityVerbose = 152 + overtypeCursorStyle = 84, + overtypeOnPaste = 85, + overviewRulerBorder = 86, + overviewRulerLanes = 87, + padding = 88, + pasteAs = 89, + parameterHints = 90, + peekWidgetDefaultFocus = 91, + placeholder = 92, + definitionLinkOpensInPeek = 93, + quickSuggestions = 94, + quickSuggestionsDelay = 95, + readOnly = 96, + readOnlyMessage = 97, + renameOnType = 98, + renderControlCharacters = 99, + renderFinalNewline = 100, + renderLineHighlight = 101, + renderLineHighlightOnlyWhenFocus = 102, + renderValidationDecorations = 103, + renderWhitespace = 104, + revealHorizontalRightPadding = 105, + roundedSelection = 106, + rulers = 107, + scrollbar = 108, + scrollBeyondLastColumn = 109, + scrollBeyondLastLine = 110, + scrollPredominantAxis = 111, + selectionClipboard = 112, + selectionHighlight = 113, + selectOnLineNumbers = 114, + showFoldingControls = 115, + showUnused = 116, + snippetSuggestions = 117, + smartSelect = 118, + smoothScrolling = 119, + stickyScroll = 120, + stickyTabStops = 121, + stopRenderingLineAfter = 122, + suggest = 123, + suggestFontSize = 124, + suggestLineHeight = 125, + suggestOnTriggerCharacters = 126, + suggestSelection = 127, + tabCompletion = 128, + tabIndex = 129, + unicodeHighlighting = 130, + unusualLineTerminators = 131, + useShadowDOM = 132, + useTabStops = 133, + wordBreak = 134, + wordSegmenterLocales = 135, + wordSeparators = 136, + wordWrap = 137, + wordWrapBreakAfterCharacters = 138, + wordWrapBreakBeforeCharacters = 139, + wordWrapColumn = 140, + wordWrapOverride1 = 141, + wordWrapOverride2 = 142, + wrappingIndent = 143, + wrappingStrategy = 144, + showDeprecated = 145, + inlayHints = 146, + effectiveCursorStyle = 147, + editorClassName = 148, + pixelRatio = 149, + tabFocusMode = 150, + layoutInfo = 151, + wrappingInfo = 152, + defaultColorDecorators = 153, + colorDecoratorsActivatedOn = 154, + inlineCompletionsAccessibilityVerbose = 155 } /** diff --git a/src/vs/editor/contrib/clipboard/browser/clipboard.ts b/src/vs/editor/contrib/clipboard/browser/clipboard.ts index 5add0823743..15fc67e770b 100644 --- a/src/vs/editor/contrib/clipboard/browser/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/browser/clipboard.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as browser from '../../../../base/browser/browser.js'; -import { getActiveDocument, getActiveWindow, isHTMLElement } from '../../../../base/browser/dom.js'; +import { getActiveDocument } from '../../../../base/browser/dom.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import * as platform from '../../../../base/common/platform.js'; import * as nls from '../../../../nls.js'; @@ -240,15 +240,14 @@ if (PasteAction) { // Since we can not call execCommand('paste') on a dom node with edit context set // we added a hidden text area that receives the paste execution // see nativeEditContext.ts for more details - const textAreaDomNode = NativeEditContextRegistry.getTextArea(focusedEditor.getId()); - if (textAreaDomNode) { - const currentFocusedElement = getActiveWindow().document.activeElement; - textAreaDomNode.focus(); + const nativeEditContext = NativeEditContextRegistry.get(focusedEditor.getId()); + if (nativeEditContext) { + const textArea = nativeEditContext.textArea; + nativeEditContext.onWillPaste(); + textArea.focus(); result = focusedEditor.getContainerDomNode().ownerDocument.execCommand('paste'); - textAreaDomNode.textContent = ''; - if (isHTMLElement(currentFocusedElement)) { - currentFocusedElement.focus(); - } + textArea.domNode.textContent = ''; + nativeEditContext.domNode.focus(); } else { result = false; } diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index 873313c82b3..ed17348099a 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -684,6 +684,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi return editIndex; } } + return 0; } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts index 0a41954d913..5a9273456c1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts @@ -13,7 +13,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ICodeEditor } from '../../../../browser/editorBrowser.js'; -import { EditorAction, ServicesAccessor } from '../../../../browser/editorExtensions.js'; +import { EditorAction, EditorCommand, ServicesAccessor } from '../../../../browser/editorExtensions.js'; import { EditorContextKeys } from '../../../../common/editorContextKeys.js'; import { Context as SuggestContext } from '../../../suggest/browser/suggest.js'; import { inlineSuggestCommitId, showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from './commandIds.js'; @@ -103,19 +103,17 @@ export class ExplicitTriggerInlineEditAction extends EditorAction { } } -export class TriggerInlineEditAction extends Action2 { +export class TriggerInlineEditAction extends EditorCommand { constructor() { super({ id: 'editor.action.inlineSuggest.triggerInlineEdit', - title: nls.localize2('action.inlineSuggest.trigger.inlineEdit', "Trigger Inline Edit"), precondition: EditorContextKeys.writable, - f1: false, }); } - public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + public override async runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: { triggerKind?: 'automatic' | 'explicit' }): Promise { const controller = InlineCompletionsController.get(editor); - await controller?.model.get()?.triggerExplicitly(undefined, true); + await controller?.model.get()?.trigger(undefined, true); } } @@ -309,7 +307,7 @@ export class ToggleAlwaysShowInlineSuggestionToolbar extends Action2 { }); } - public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + public async run(accessor: ServicesAccessor): Promise { const configService = accessor.get(IConfigurationService); const currentValue = configService.getValue<'always' | 'onHover'>('editor.inlineSuggest.showToolbar'); const newValue = currentValue === 'always' ? 'onHover' : 'always'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts index 9221334c230..36be1d4e5f9 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts @@ -6,7 +6,7 @@ import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { registerAction2 } from '../../../../platform/actions/common/actions.js'; import { wrapInHotClass1 } from '../../../../platform/observable/common/wrapInHotClass.js'; -import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from '../../../browser/editorExtensions.js'; +import { EditorContributionInstantiation, registerEditorAction, registerEditorCommand, registerEditorContribution } from '../../../browser/editorExtensions.js'; import { HoverParticipantRegistry } from '../../hover/browser/hoverTypes.js'; import { AcceptInlineCompletion, AcceptNextLineOfInlineCompletion, AcceptNextWordOfInlineCompletion, DevExtractReproSample, HideInlineCompletion, JumpToNextInlineEdit, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, ToggleAlwaysShowInlineSuggestionToolbar, ExplicitTriggerInlineEditAction, TriggerInlineSuggestionAction, TriggerInlineEditAction } from './controller/commands.js'; import { InlineCompletionsController } from './controller/inlineCompletionsController.js'; @@ -21,7 +21,7 @@ registerEditorContribution(InlineCompletionsController.ID, wrapInHotClass1(Inlin registerEditorAction(TriggerInlineSuggestionAction); registerEditorAction(ExplicitTriggerInlineEditAction); -registerAction2(TriggerInlineEditAction); +registerEditorCommand(new TriggerInlineEditAction()); registerEditorAction(ShowNextInlineSuggestionAction); registerEditorAction(ShowPreviousInlineSuggestionAction); registerEditorAction(AcceptNextWordOfInlineCompletion); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index f09dea308b1..3de779b8364 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -14,6 +14,7 @@ import { ICommandService } from '../../../../../platform/commands/common/command import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../browser/observableCodeEditor.js'; +import { EditorOption } from '../../../../common/config/editorOptions.js'; import { CursorColumns } from '../../../../common/core/cursorColumns.js'; import { EditOperation } from '../../../../common/core/editOperation.js'; import { LineRange } from '../../../../common/core/lineRange.js'; @@ -53,6 +54,8 @@ export class InlineCompletionsModel extends Disposable { private readonly _editorObs = observableCodeEditor(this._editor); + private readonly onlyShowWhenCloseToCursor = this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !!v.edits.experimental.onlyShowWhenCloseToCursor); + constructor( public readonly textModel: ITextModel, private readonly _selectedSuggestItem: IObservable, @@ -87,6 +90,11 @@ export class InlineCompletionsModel extends Disposable { } } })); + + this._register(autorun(reader => { + this._editorObs.versionId.read(reader); + this._inAcceptFlow.set(false, undefined); + })); } public debugGetSelectedSuggestItem(): IObservable { @@ -201,8 +209,13 @@ export class InlineCompletionsModel extends Disposable { return this._source.fetch(cursorPosition, context, itemToPreserve); }); - public async trigger(tx?: ITransaction): Promise { - this._isActive.set(true, tx); + public async trigger(tx?: ITransaction, onlyFetchInlineEdits: boolean = false): Promise { + subtransaction(tx, tx => { + if (onlyFetchInlineEdits) { + this._onlyRequestInlineEditsSignal.trigger(tx); + } + this._isActive.set(true, tx); + }); await this._fetchInlineCompletionsPromise.get(); } @@ -344,6 +357,10 @@ export class InlineCompletionsModel extends Disposable { const cursorAtInlineEdit = LineRange.fromRangeInclusive(edit.range).addMargin(1, 1).contains(cursorPos.lineNumber); const cursorDist = LineRange.fromRange(edit.range).distanceToLine(this._primaryPosition.read(reader).lineNumber); + + if (this.onlyShowWhenCloseToCursor.read(reader) && cursorDist > 3 && !item.inlineEdit.request.isExplicitRequest && !this._inAcceptFlow.read(reader)) { + return undefined; + } const disableCollapsing = true; const currentItemIsCollapsed = !disableCollapsing && (cursorDist > 1 && this._collapsedInlineEditId.read(reader) === item.inlineEdit.semanticId); @@ -579,6 +596,8 @@ export class InlineCompletionsModel extends Disposable { .then(undefined, onUnexpectedExternalError); completion.source.removeRef(); } + + this._inAcceptFlow.set(true, undefined); } public async acceptNextWord(editor: ICodeEditor): Promise { @@ -711,6 +730,7 @@ export class InlineCompletionsModel extends Disposable { } private _jumpedTo = observableValue(this, false); + private _inAcceptFlow = observableValue(this, false); public jump(): void { const s = this.inlineEditState.get(); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index ea817862159..79b3b8faacb 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -216,6 +216,10 @@ class UpdateRequest { || this.context.triggerKind === InlineCompletionTriggerKind.Explicit) && this.versionId === other.versionId; } + + public get isExplicitRequest() { + return this.context.triggerKind === InlineCompletionTriggerKind.Explicit; + } } class UpdateOperation implements IDisposable { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts index 5339284ec69..faddbf04b09 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineDiffView.ts @@ -9,7 +9,7 @@ import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { rangeIsSingleLine } from '../../../../../browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.js'; import { LineSource, renderLines, RenderOptions } from '../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; -import { diffLineDeleteDecorationBackgroundWithIndicator, diffLineDeleteDecorationBackground, diffLineAddDecorationBackgroundWithIndicator, diffLineAddDecorationBackground, diffWholeLineAddDecoration, diffAddDecorationEmpty, diffAddDecoration } from '../../../../../browser/widget/diffEditor/registrations.contribution.js'; +import { diffAddDecoration } from '../../../../../browser/widget/diffEditor/registrations.contribution.js'; import { applyViewZones, IObservableViewZone } from '../../../../../browser/widget/diffEditor/utils.js'; import { EditorOption } from '../../../../../common/config/editorOptions.js'; import { Range } from '../../../../../common/core/range.js'; @@ -113,31 +113,67 @@ export class OriginalEditorInlineDiffView extends Disposable { const modified = diff.modifiedText; const showInline = diff.mode === 'mixedLines'; - const renderIndicators = false; const showEmptyDecorations = true; const originalDecorations: IModelDeltaDecoration[] = []; const modifiedDecorations: IModelDeltaDecoration[] = []; + const diffLineAddDecorationBackground = ModelDecorationOptions.register({ + className: 'inlineCompletions-line-insert', + description: 'line-insert', + isWholeLine: true, + marginClassName: 'gutter-insert', + }); + + const diffLineDeleteDecorationBackground = ModelDecorationOptions.register({ + className: 'inlineCompletions-line-delete', + description: 'line-delete', + isWholeLine: true, + marginClassName: 'gutter-delete', + }); + + const diffWholeLineDeleteDecoration = ModelDecorationOptions.register({ + className: 'inlineCompletions-char-delete', + description: 'char-delete', + isWholeLine: false, + }); + + const diffWholeLineAddDecoration = ModelDecorationOptions.register({ + className: 'inlineCompletions-char-insert', + description: 'char-insert', + isWholeLine: true, + }); + + const diffAddDecoration = ModelDecorationOptions.register({ + className: 'inlineCompletions-char-insert', + description: 'char-insert', + shouldFillLineOnLineBreak: true, + }); + + const diffAddDecorationEmpty = ModelDecorationOptions.register({ + className: 'inlineCompletions-char-insert diff-range-empty', + description: 'char-insert diff-range-empty', + }); + for (const m of diff.diff) { const showFullLineDecorations = true; if (showFullLineDecorations) { if (!m.original.isEmpty) { - originalDecorations.push({ range: m.original.toInclusiveRange()!, options: renderIndicators ? diffLineDeleteDecorationBackgroundWithIndicator : diffLineDeleteDecorationBackground }); + originalDecorations.push({ + range: m.original.toInclusiveRange()!, + options: diffLineDeleteDecorationBackground, + }); } if (!m.modified.isEmpty) { - modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: renderIndicators ? diffLineAddDecorationBackgroundWithIndicator : diffLineAddDecorationBackground }); + modifiedDecorations.push({ + range: m.modified.toInclusiveRange()!, + options: diffLineAddDecorationBackground, + }); } } if (m.modified.isEmpty || m.original.isEmpty) { if (!m.original.isEmpty) { - const diffWholeLineDeleteDecoration = ModelDecorationOptions.register({ - className: 'char-delete', - description: 'char-delete', - isWholeLine: false, - }); - originalDecorations.push({ range: m.original.toInclusiveRange()!, options: diffWholeLineDeleteDecoration }); } if (!m.modified.isEmpty) { @@ -149,11 +185,12 @@ export class OriginalEditorInlineDiffView extends Disposable { // Don't show empty markers outside the line range if (m.original.contains(i.originalRange.startLineNumber)) { originalDecorations.push({ - range: i.originalRange, options: { + range: i.originalRange, + options: { description: 'char-delete', shouldFillLineOnLineBreak: false, className: classNames( - 'char-delete', + 'inlineCompletions-char-delete', (i.originalRange.isEmpty() && showEmptyDecorations && !useInlineDiff) && 'diff-range-empty' ), inlineClassName: useInlineDiff ? 'strike-through' : null, @@ -162,7 +199,12 @@ export class OriginalEditorInlineDiffView extends Disposable { }); } if (m.modified.contains(i.modifiedRange.startLineNumber)) { - modifiedDecorations.push({ range: i.modifiedRange, options: (i.modifiedRange.isEmpty() && showEmptyDecorations && !useInlineDiff) ? diffAddDecorationEmpty : diffAddDecoration }); + modifiedDecorations.push({ + range: i.modifiedRange, + options: (i.modifiedRange.isEmpty() && showEmptyDecorations && !useInlineDiff) + ? diffAddDecorationEmpty + : diffAddDecoration + }); } if (useInlineDiff) { const insertedText = modified.getValueOfRange(i.modifiedRange); @@ -172,7 +214,7 @@ export class OriginalEditorInlineDiffView extends Disposable { description: 'inserted-text', before: { content: insertedText, - inlineClassName: 'char-insert', + inlineClassName: 'inlineCompletions-char-insert', }, zIndex: 2, showIfCollapsed: true, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css index b955689cb7c..e4b742e9391 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css @@ -141,4 +141,29 @@ .strike-through { text-decoration: line-through; } + + .inlineCompletions-line-insert { + background: var(--vscode-inlineEdit-modifiedChangedLineBackground); + } + + .inlineCompletions-line-delete { + background: var(--vscode-inlineEdit-originalChangedLineBackground); + } + + .inlineCompletions-char-insert { + background: var(--vscode-inlineEdit-modifiedChangedTextBackground); + } + + .inlineCompletions-char-delete { + background: var(--vscode-inlineEdit-originalChangedTextBackground); + } + + .inlineCompletions-char-delete.diff-range-empty { + margin-left: -1px; + border-left: solid var(--vscode-inlineEdit-originalChangedTextBackground) 3px; + } + + .inlineCompletions-char-insert.diff-range-empty { + border-left: solid var(--vscode-inlineEdit-modifiedChangedTextBackground) 3px; + } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts index 65987b8e7b3..8268aab9a2e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts @@ -20,7 +20,7 @@ import { lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../ import { TextModel } from '../../../../../common/model/textModel.js'; import './inlineEditsView.css'; import { IOriginalEditorInlineDiffViewState, OriginalEditorInlineDiffView } from './inlineDiffView.js'; -import { applyEditToModifiedRangeMappings, createReindentEdit, maxLeftInRange, PathBuilder, Point, StatusBarViewItem } from './utils.js'; +import { applyEditToModifiedRangeMappings, createReindentEdit, maxContentWidthInRange, PathBuilder, Point, StatusBarViewItem } from './utils.js'; import { IInlineEditsIndicatorState, InlineEditsIndicator } from './inlineEditsIndicatorView.js'; import { darken, lighten, registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; import { diffInserted, diffRemoved } from '../../../../../../platform/theme/common/colorRegistry.js'; @@ -33,6 +33,8 @@ import { editorLineHighlightBorder } from '../../../../../common/core/editorColo import { ActionViewItem } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; import { InlineEditWithChanges } from './inlineEditsViewAndDiffProducer.js'; +import { OffsetRange } from '../../../../../common/core/offsetRange.js'; +import { Color } from '../../../../../../base/common/color.js'; export const originalBackgroundColor = registerColor( 'inlineEdit.originalBackground', @@ -47,6 +49,34 @@ export const modifiedBackgroundColor = registerColor( true ); +export const originalChangedLineBackgroundColor = registerColor( + 'inlineEdit.originalChangedLineBackground', + Color.transparent, + '', + true +); + +export const originalChangedTextOverlayColor = registerColor( + 'inlineEdit.originalChangedTextBackground', + diffRemoved, + '', + true +); + +export const modifiedChangedLineBackgroundColor = registerColor( + 'inlineEdit.modifiedChangedLineBackground', + Color.transparent, + '', + true +); + +export const modifiedChangedTextOverlayColor = registerColor( + 'inlineEdit.modifiedChangedTextBackground', + diffInserted, + '', + true +); + export const border = registerColor( 'inlineEdit.border', { @@ -69,12 +99,12 @@ export class InlineEditsView extends Disposable { left: '0px', }, }, [ - svgElem('svg@svg', { style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, }, []), + svgElem('svg@svg', { transform: 'translate(-0.5 -0.5)', style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, }, []), h('div.editorContainer@editorContainer', { style: { position: 'absolute' } }, [ h('div.preview@editor', { style: {} }), h('div.toolbar@toolbar', { style: {} }), ]), - svgElem('svg@svg2', { style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, }, []), + svgElem('svg@svg2', { transform: 'translate(-0.5 -0.5)', style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, }, []), ]); private readonly _useMixedLinesDiff = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.experimental.useMixedLinesDiff); @@ -96,9 +126,9 @@ export class InlineEditsView extends Disposable { position: constObservable(null), allowEditorOverflow: false, minContentWidthInPx: derived(reader => { - const x = this._previewEditorLeft.read(reader)?.left; + const x = this._previewEditorLayoutInfo.read(reader)?.previewEditorLeft; if (x === undefined) { return 0; } - const width = this._previewEditorWidth.read(reader); + const width = this._previewEditorWidth.read(reader) + 70; return x + width; }), })); @@ -131,13 +161,11 @@ export class InlineEditsView extends Disposable { pathBuilder2.lineTo(layoutInfo.edit1.deltaX(width)); pathBuilder2.lineTo(layoutInfo.edit2.deltaX(width)); pathBuilder2.lineTo(layoutInfo.edit2); - pathBuilder2.curveTo2(layoutInfo.edit2.deltaX(-20), layoutInfo.code2.deltaX(20), layoutInfo.code2.deltaX(0)); + if (layoutInfo.edit2.y !== layoutInfo.code2.y) { + pathBuilder2.curveTo2(layoutInfo.edit2.deltaX(-20), layoutInfo.code2.deltaX(20), layoutInfo.code2.deltaX(0)); + } pathBuilder2.lineTo(layoutInfo.code2); - const pathBuilder3 = new PathBuilder(); - pathBuilder3.moveTo(layoutInfo.code1); - pathBuilder3.lineTo(layoutInfo.code2); - const pathBuilder4 = new PathBuilder(); pathBuilder4.moveTo(layoutInfo.code1); pathBuilder4.lineTo(layoutInfo.code1.deltaX(1000)); @@ -150,6 +178,7 @@ export class InlineEditsView extends Disposable { path1.style.stroke = 'var(--vscode-inlineEdit-border)'; path1.style.strokeWidth = '1px'; + const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path2.setAttribute('d', pathBuilder2.build()); path2.style.fill = 'var(--vscode-inlineEdit-modifiedBackground, transparent)'; @@ -161,17 +190,58 @@ export class InlineEditsView extends Disposable { pathModifiedBackground.style.fill = 'var(--vscode-editor-background, transparent)'; pathModifiedBackground.style.strokeWidth = '1px'; - const path3 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - path3.setAttribute('d', pathBuilder3.build()); - path3.style.stroke = 'var(--vscode-inlineEdit-border)'; - path3.style.strokeWidth = '1px'; + + + const elements: SVGElement[] = []; + if (layoutInfo.shouldShowShadow) { + const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); + const linearGradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient'); + linearGradient.setAttribute('id', 'gradient'); + linearGradient.setAttribute('x1', '0%'); + linearGradient.setAttribute('x2', '100%'); + + const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); + stop1.setAttribute('offset', '0%'); + stop1.setAttribute('style', 'stop-color:var(--vscode-inlineEdit-border);stop-opacity:0'); + + const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); + stop2.setAttribute('offset', '100%'); + stop2.setAttribute('style', 'stop-color:var(--vscode-inlineEdit-border);stop-opacity:1'); + + linearGradient.appendChild(stop1); + linearGradient.appendChild(stop2); + defs.appendChild(linearGradient); + + const width = 6; + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('x', `${layoutInfo.code1.x - width}`); + rect.setAttribute('y', `${layoutInfo.code1.y}`); + rect.setAttribute('width', `${width}`); + rect.setAttribute('height', `${layoutInfo.code2.y - layoutInfo.code1.y}`); + rect.setAttribute('fill', 'url(#gradient)'); + rect.style.strokeWidth = '0'; + rect.style.stroke = 'transparent'; + + elements.push(defs); + elements.push(rect); + } else { + const pathBuilder3 = new PathBuilder(); + pathBuilder3.moveTo(layoutInfo.code1); + pathBuilder3.lineTo(layoutInfo.code2); + + const path3 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path3.setAttribute('d', pathBuilder3.build()); + path3.style.stroke = 'var(--vscode-inlineEdit-border)'; + path3.style.strokeWidth = '1px'; + elements.push(path3); + } const path4 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path4.setAttribute('d', pathBuilder4.build()); path4.style.fill = 'var(--vscode-editor-background, transparent)'; this._elements.svg.replaceChildren(path4, pathModifiedBackground); - this._elements.svg2.replaceChildren(path1, path2, path3); + this._elements.svg2.replaceChildren(path1, path2, ...elements); this._elements.editorContainer.style.top = `${topEdit.y}px`; this._elements.editorContainer.style.left = `${topEdit.x}px`; @@ -340,6 +410,7 @@ export class InlineEditsView extends Disposable { if (!uiState) { return; } + this._previewTextModel.setLanguage(this._editor.getModel()!.getLanguageId()); this._previewTextModel.setValue(uiState.newText); const range = uiState.edit.originalLineRange; @@ -360,23 +431,7 @@ export class InlineEditsView extends Disposable { if (!edit) { return 0; } this._updatePreviewEditor.read(reader); - return maxLeftInRange(this._previewEditorObs, edit.modifiedLineRange, reader); - }); - - private readonly _previewEditorLeft = derived(this, reader => { - const state = this._uiState.read(reader); - if (!state) { return null; } - - const maxLeft = maxLeftInRange(this._editorObs, state.originalDisplayRange, reader); - const contentLeft = this._editorObs.layoutInfoContentLeft.read(reader); - - const editorLayoutInfo = this._editorObs.layoutInfo.read(reader); - - const minLeft = (editorLayoutInfo.width - editorLayoutInfo.minimap.minimapWidth) * 0.65; - - const scrollLeft = this._editorObs.scrollLeft.read(reader); - - return { left: Math.min(contentLeft + maxLeft, minLeft + scrollLeft) }; + return maxContentWidthInRange(this._previewEditorObs, edit.modifiedLineRange, reader); }); /** @@ -384,13 +439,32 @@ export class InlineEditsView extends Disposable { */ private readonly _previewEditorLayoutInfo = derived(this, (reader) => { const inlineEdit = this._edit.read(reader); - if (!inlineEdit) { return null; } + if (!inlineEdit) { + return null; + } + const state = this._uiState.read(reader); + if (!state) { + return null; + } const range = inlineEdit.originalLineRange; const scrollLeft = this._editorObs.scrollLeft.read(reader); - const left = this._previewEditorLeft.read(reader)!.left + 20 - scrollLeft; + const maxContentWidth = maxContentWidthInRange(this._editorObs, state.originalDisplayRange, reader); + const contentLeft = this._editorObs.layoutInfoContentLeft.read(reader); + const editorLayoutInfo = this._editorObs.layoutInfo.read(reader); + const previewEditorWidth = this._previewEditorWidth.read(reader); + const textAreaWidth = (editorLayoutInfo.width - editorLayoutInfo.contentLeft - editorLayoutInfo.minimap.minimapWidth); + const maxPreviewEditorLeft = Math.max( + textAreaWidth * 0.65 + scrollLeft, + textAreaWidth - previewEditorWidth - 60, + ); + + const previewEditorLeft = Math.min(contentLeft + maxContentWidth, maxPreviewEditorLeft); + const dist = maxPreviewEditorLeft - previewEditorLeft; + + const left = previewEditorLeft + 20 - scrollLeft; const selectionTop = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); const selectionBottom = this._editor.getTopForLineNumber(range.endLineNumberExclusive) - this._editorObs.scrollTop.read(reader); @@ -403,7 +477,14 @@ export class InlineEditsView extends Disposable { const codeStart2 = new Point(codeLeft, selectionBottom); const codeHeight = selectionBottom - selectionTop; - const codeEditDist = 60; + const codeEditDistRange = + inlineEdit.modifiedLineRange.length === inlineEdit.originalLineRange.length + ? new OffsetRange(4, 61) + : new OffsetRange(60, 61); + + const clipped = dist === 0; + + const codeEditDist = codeEditDistRange.clip(dist); const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.modifiedLineRange.length; const edit1 = new Point(left + codeEditDist, selectionTop); @@ -419,6 +500,8 @@ export class InlineEditsView extends Disposable { edit1, edit2, editHeight, + previewEditorLeft, + shouldShowShadow: clipped, }; }); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts index a5bae20bbfa..1a50a6f3802 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -20,26 +20,26 @@ import { RangeMapping } from '../../../../../common/diff/rangeMapping.js'; import { Range } from '../../../../../common/core/range.js'; import { Position } from '../../../../../common/core/position.js'; -export function maxLeftInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader): number { +export function maxContentWidthInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader): number { editor.layoutInfo.read(reader); editor.value.read(reader); const model = editor.model.read(reader); if (!model) { return 0; } - let maxLeft = 0; + let maxContentWidth = 0; editor.scrollTop.read(reader); for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) { const column = model.getLineMaxColumn(i); - const left = editor.editor.getOffsetForColumn(i, column); - maxLeft = Math.max(maxLeft, left); + const lineContentWidth = editor.editor.getOffsetForColumn(i, column); + maxContentWidth = Math.max(maxContentWidth, lineContentWidth); } const lines = range.mapToLineArray(l => model.getLineContent(l)); - if (maxLeft < 5 && lines.some(l => l.length > 0) && model.uri.scheme !== 'file') { + if (maxContentWidth < 5 && lines.some(l => l.length > 0) && model.uri.scheme !== 'file') { console.error('unexpected width'); } - return maxLeft; + return maxContentWidth; } export class StatusBarViewItem extends MenuEntryActionViewItem { @@ -76,6 +76,10 @@ export class Point { public deltaX(delta: number): Point { return new Point(this.x + delta, this.y); } + + public deltaY(delta: number): Point { + return new Point(this.x, this.y + delta); + } } export class UniqueUriGenerator { diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.ts b/src/vs/editor/contrib/rename/browser/renameWidget.ts index 6e93d0862fd..e5a7bc71497 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.ts +++ b/src/vs/editor/contrib/rename/browser/renameWidget.ts @@ -995,7 +995,7 @@ class InputWithButton implements IDisposable { setStopButton() { this._buttonState = 'stop'; - this._stopIcon ??= renderIcon(Codicon.primitiveSquare); + this._stopIcon ??= renderIcon(Codicon.stopCircle); dom.clearNode(this.button); this.button.appendChild(this._stopIcon); this.button.setAttribute('aria-label', 'Cancel generating new name suggestions'); diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 2eb1303c28c..fe7c5e59c46 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -28,6 +28,7 @@ import { OutgoingViewModelEventKind } from '../../../common/viewModelEventDispat import { ITestCodeEditor, TestCodeEditorInstantiationOptions, createCodeEditorServices, instantiateTestCodeEditor, withTestCodeEditor } from '../testCodeEditor.js'; import { IRelaxedTextModelCreationOptions, createTextModel, instantiateTextModel } from '../../common/testTextModel.js'; import { TestInstantiationService } from '../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { InputMode } from '../../../common/inputMode.js'; // --------- utils @@ -6451,3 +6452,183 @@ suite('Undo stops', () => { model.dispose(); }); }); + +suite('Overtype Mode', () => { + + setup(() => { + InputMode.setInputMode('overtype'); + }); + + teardown(() => { + InputMode.setInputMode('insert'); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('simple type', () => { + const model = createTextModel( + [ + '123456789', + '123456789', + ].join('\n'), + undefined, + { + insertSpaces: false, + } + ); + + withTestCodeEditor(model, {}, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); + viewModel.type('a', 'keyboard'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), [ + '12a456789', + '123456789', + ].join('\n'), 'assert1'); + + viewModel.setSelections('test', [new Selection(1, 9, 1, 9)]); + viewModel.type('bbb', 'keyboard'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), [ + '12a45678bbb', + '123456789', + ].join('\n'), 'assert2'); + }); + + model.dispose(); + }); + + test('multi-line selection type', () => { + const model = createTextModel( + [ + '123456789', + '123456789', + ].join('\n'), + undefined, + { + insertSpaces: false, + } + ); + + withTestCodeEditor(model, {}, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 5, 2, 3)]); + viewModel.type('cc', 'keyboard'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), [ + '1234cc456789', + ].join('\n'), 'assert1'); + }); + + model.dispose(); + }); + + test('simple paste', () => { + const model = createTextModel( + [ + '123456789', + '123456789', + ].join('\n'), + undefined, + { + insertSpaces: false, + } + ); + + withTestCodeEditor(model, {}, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 5, 1, 5)]); + viewModel.paste('cc', false); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), [ + '1234cc789', + '123456789', + ].join('\n'), 'assert1'); + + viewModel.setSelections('test', [new Selection(1, 5, 1, 5)]); + viewModel.paste('dddddddd', false); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), [ + '1234dddddddd', + '123456789', + ].join('\n'), 'assert2'); + }); + + model.dispose(); + }); + + test('multi-line selection paste', () => { + const model = createTextModel( + [ + '123456789', + '123456789', + ].join('\n'), + undefined, + { + insertSpaces: false, + } + ); + + withTestCodeEditor(model, {}, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 5, 2, 3)]); + viewModel.paste('cc', false); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), [ + '1234cc456789', + ].join('\n'), 'assert1'); + }); + + model.dispose(); + }); + + test('paste multi-line text', () => { + const model = createTextModel( + [ + '123456789', + '123456789', + ].join('\n'), + undefined, + { + insertSpaces: false, + } + ); + + withTestCodeEditor(model, {}, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 5, 1, 5)]); + viewModel.paste([ + 'aaaaaaa', + 'bbbbbbb' + ].join('\n'), false); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), [ + '1234aaaaaaa', + 'bbbbbbb', + '123456789', + ].join('\n'), 'assert1'); + }); + + model.dispose(); + }); + + test('composition type', () => { + const model = createTextModel( + [ + '123456789', + '123456789', + ].join('\n'), + undefined, + { + insertSpaces: false, + } + ); + + withTestCodeEditor(model, {}, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 5, 1, 5)]); + viewModel.startComposition(); + viewModel.compositionType('セ', 0, 0, 0, 'keyboard'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), [ + '1234セ56789', + '123456789', + ].join('\n'), 'assert1'); + + viewModel.endComposition('keyboard'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), [ + '1234セ6789', + '123456789', + ].join('\n'), 'assert1'); + }); + + model.dispose(); + }); +}); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index f9389176183..6834298bdb0 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3284,10 +3284,19 @@ declare namespace monaco.editor { */ cursorSmoothCaretAnimation?: 'off' | 'explicit' | 'on'; /** - * Control the cursor style, either 'block' or 'line'. + * Control the cursor style in insert mode. * Defaults to 'line'. */ cursorStyle?: 'line' | 'block' | 'underline' | 'line-thin' | 'block-outline' | 'underline-thin'; + /** + * Control the cursor style in overtype mode. + * Defaults to 'block'. + */ + overtypeCursorStyle?: 'line' | 'block' | 'underline' | 'line-thin' | 'block-outline' | 'underline-thin'; + /** + * Controls whether paste in overtype mode should overwrite or insert. + */ + overtypeOnPaste?: boolean; /** * Control the width of the cursor when cursorStyle is set to 'line' */ @@ -4599,6 +4608,7 @@ declare namespace monaco.editor { enabled?: boolean; useMixedLinesDiff?: 'never' | 'whenPossible' | 'afterJumpWhenPossible'; useInterleavedLinesDiff?: 'never' | 'always' | 'afterJump'; + onlyShowWhenCloseToCursor?: boolean; }; }; } @@ -4963,75 +4973,78 @@ declare namespace monaco.editor { multiCursorLimit = 81, occurrencesHighlight = 82, occurrencesHighlightDelay = 83, - overviewRulerBorder = 84, - overviewRulerLanes = 85, - padding = 86, - pasteAs = 87, - parameterHints = 88, - peekWidgetDefaultFocus = 89, - placeholder = 90, - definitionLinkOpensInPeek = 91, - quickSuggestions = 92, - quickSuggestionsDelay = 93, - readOnly = 94, - readOnlyMessage = 95, - renameOnType = 96, - renderControlCharacters = 97, - renderFinalNewline = 98, - renderLineHighlight = 99, - renderLineHighlightOnlyWhenFocus = 100, - renderValidationDecorations = 101, - renderWhitespace = 102, - revealHorizontalRightPadding = 103, - roundedSelection = 104, - rulers = 105, - scrollbar = 106, - scrollBeyondLastColumn = 107, - scrollBeyondLastLine = 108, - scrollPredominantAxis = 109, - selectionClipboard = 110, - selectionHighlight = 111, - selectOnLineNumbers = 112, - showFoldingControls = 113, - showUnused = 114, - snippetSuggestions = 115, - smartSelect = 116, - smoothScrolling = 117, - stickyScroll = 118, - stickyTabStops = 119, - stopRenderingLineAfter = 120, - suggest = 121, - suggestFontSize = 122, - suggestLineHeight = 123, - suggestOnTriggerCharacters = 124, - suggestSelection = 125, - tabCompletion = 126, - tabIndex = 127, - unicodeHighlighting = 128, - unusualLineTerminators = 129, - useShadowDOM = 130, - useTabStops = 131, - wordBreak = 132, - wordSegmenterLocales = 133, - wordSeparators = 134, - wordWrap = 135, - wordWrapBreakAfterCharacters = 136, - wordWrapBreakBeforeCharacters = 137, - wordWrapColumn = 138, - wordWrapOverride1 = 139, - wordWrapOverride2 = 140, - wrappingIndent = 141, - wrappingStrategy = 142, - showDeprecated = 143, - inlayHints = 144, - editorClassName = 145, - pixelRatio = 146, - tabFocusMode = 147, - layoutInfo = 148, - wrappingInfo = 149, - defaultColorDecorators = 150, - colorDecoratorsActivatedOn = 151, - inlineCompletionsAccessibilityVerbose = 152 + overtypeCursorStyle = 84, + overtypeOnPaste = 85, + overviewRulerBorder = 86, + overviewRulerLanes = 87, + padding = 88, + pasteAs = 89, + parameterHints = 90, + peekWidgetDefaultFocus = 91, + placeholder = 92, + definitionLinkOpensInPeek = 93, + quickSuggestions = 94, + quickSuggestionsDelay = 95, + readOnly = 96, + readOnlyMessage = 97, + renameOnType = 98, + renderControlCharacters = 99, + renderFinalNewline = 100, + renderLineHighlight = 101, + renderLineHighlightOnlyWhenFocus = 102, + renderValidationDecorations = 103, + renderWhitespace = 104, + revealHorizontalRightPadding = 105, + roundedSelection = 106, + rulers = 107, + scrollbar = 108, + scrollBeyondLastColumn = 109, + scrollBeyondLastLine = 110, + scrollPredominantAxis = 111, + selectionClipboard = 112, + selectionHighlight = 113, + selectOnLineNumbers = 114, + showFoldingControls = 115, + showUnused = 116, + snippetSuggestions = 117, + smartSelect = 118, + smoothScrolling = 119, + stickyScroll = 120, + stickyTabStops = 121, + stopRenderingLineAfter = 122, + suggest = 123, + suggestFontSize = 124, + suggestLineHeight = 125, + suggestOnTriggerCharacters = 126, + suggestSelection = 127, + tabCompletion = 128, + tabIndex = 129, + unicodeHighlighting = 130, + unusualLineTerminators = 131, + useShadowDOM = 132, + useTabStops = 133, + wordBreak = 134, + wordSegmenterLocales = 135, + wordSeparators = 136, + wordWrap = 137, + wordWrapBreakAfterCharacters = 138, + wordWrapBreakBeforeCharacters = 139, + wordWrapColumn = 140, + wordWrapOverride1 = 141, + wordWrapOverride2 = 142, + wrappingIndent = 143, + wrappingStrategy = 144, + showDeprecated = 145, + inlayHints = 146, + effectiveCursorStyle = 147, + editorClassName = 148, + pixelRatio = 149, + tabFocusMode = 150, + layoutInfo = 151, + wrappingInfo = 152, + defaultColorDecorators = 153, + colorDecoratorsActivatedOn = 154, + inlineCompletionsAccessibilityVerbose = 155 } export const EditorOptions: { @@ -5066,6 +5079,7 @@ declare namespace monaco.editor { cursorBlinking: IEditorOption; cursorSmoothCaretAnimation: IEditorOption; cursorStyle: IEditorOption; + overtypeCursorStyle: IEditorOption; cursorSurroundingLines: IEditorOption; cursorSurroundingLinesStyle: IEditorOption; cursorWidth: IEditorOption; @@ -5121,6 +5135,7 @@ declare namespace monaco.editor { multiCursorLimit: IEditorOption; occurrencesHighlight: IEditorOption; occurrencesHighlightDelay: IEditorOption; + overtypeOnPaste: IEditorOption; overviewRulerBorder: IEditorOption; overviewRulerLanes: IEditorOption; padding: IEditorOption>>; @@ -5180,6 +5195,7 @@ declare namespace monaco.editor { wordWrapColumn: IEditorOption; wordWrapOverride1: IEditorOption; wordWrapOverride2: IEditorOption; + effectiveCursorStyle: IEditorOption; editorClassName: IEditorOption; defaultColorDecorators: IEditorOption; pixelRatio: IEditorOption; diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index cdc635fb5b2..7f00003c0f3 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -124,6 +124,7 @@ export class MenuId { static readonly SCMSourceControlInline = new MenuId('SCMSourceControlInline'); static readonly SCMSourceControlTitle = new MenuId('SCMSourceControlTitle'); static readonly SCMHistoryTitle = new MenuId('SCMHistoryTitle'); + static readonly SCMHistoryItemRefContext = new MenuId('SCMHistoryItemRefContext'); static readonly SCMTitle = new MenuId('SCMTitle'); static readonly SearchContext = new MenuId('SearchContext'); static readonly SearchActionMenu = new MenuId('SearchActionContext'); diff --git a/src/vs/platform/theme/common/colors/miscColors.ts b/src/vs/platform/theme/common/colors/miscColors.ts index 95048e2876e..f816bf493f9 100644 --- a/src/vs/platform/theme/common/colors/miscColors.ts +++ b/src/vs/platform/theme/common/colors/miscColors.ts @@ -71,3 +71,17 @@ export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.ac export const progressBarBackground = registerColor('progressBar.background', { dark: Color.fromHex('#0E70C0'), light: Color.fromHex('#0E70C0'), hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('progressBarBackground', "Background color of the progress bar that can show for long running operations.")); + +// ----- chart + +export const chartLine = registerColor('chart.line', + { dark: '#236B8E', light: '#236B8E', hcDark: '#236B8E', hcLight: '#236B8E' }, + nls.localize('chartLine', "Line color for the chart.")); + +export const chartAxis = registerColor('chart.axis', + { dark: Color.fromHex('#BFBFBF').transparent(0.4), light: Color.fromHex('#000000').transparent(0.6), hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('chartAxis', "Axis color for the chart.")); + +export const chartGuide = registerColor('chart.guide', + { dark: Color.fromHex('#BFBFBF').transparent(0.2), light: Color.fromHex('#000000').transparent(0.2), hcDark: contrastBorder, hcLight: contrastBorder }, + nls.localize('chartGuide', "Guide line for the chart.")); diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts index 90b054ad13d..e2f88fbf8c0 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts @@ -169,7 +169,6 @@ export class UtilityProcess extends Disposable { private process: ElectronUtilityProcess | undefined = undefined; private processPid: number | undefined = undefined; private configuration: IUtilityProcessConfiguration | undefined = undefined; - private killed = false; constructor( @ILogService private readonly logService: ILogService, @@ -316,11 +315,10 @@ export class UtilityProcess extends Disposable { // Exit this._register(Event.fromNodeEventEmitter(process, 'exit')(code => { - const normalizedCode = this.isNormalExit(code) ? 0 : code; - this.log(`received exit event with code ${normalizedCode}`, Severity.Info); + this.log(`received exit event with code ${code}`, Severity.Info); // Event - this._onExit.fire({ pid: this.processPid!, code: normalizedCode, signal: 'unknown' }); + this._onExit.fire({ pid: this.processPid!, code, signal: 'unknown' }); // Cleanup this.onDidExitOrCrashOrKill(); @@ -368,7 +366,7 @@ export class UtilityProcess extends Disposable { // Child process gone this._register(Event.fromNodeEventEmitter<{ details: Details }>(app, 'child-process-gone', (event, details) => ({ event, details }))(({ details }) => { - if (details.type === 'Utility' && details.name === serviceName && !this.isNormalExit(details.exitCode)) { + if (details.type === 'Utility' && details.name === serviceName) { this.log(`crashed with code ${details.exitCode} and reason '${details.reason}'`, Severity.Error); // Telemetry @@ -458,24 +456,12 @@ export class UtilityProcess extends Disposable { const killed = this.process.kill(); if (killed) { this.log('successfully killed the process', Severity.Info); - this.killed = true; this.onDidExitOrCrashOrKill(); } else { this.log('unable to kill the process', Severity.Warning); } } - private isNormalExit(exitCode: number): boolean { - if (exitCode === 0) { - return true; - } - - // Treat an exit code of 15 (SIGTERM) as a normal exit - // if we triggered the termination from process.kill() - - return this.killed && exitCode === 15 /* SIGTERM */; - } - private onDidExitOrCrashOrKill(): void { if (typeof this.processPid === 'number') { UtilityProcess.all.delete(this.processPid); diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index c774dd36e0f..f25275f7168 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -102,7 +102,7 @@ export class MainThreadCommentThread implements languages.CommentThread { return this._canReply; } - private _collapsibleState: languages.CommentThreadCollapsibleState | undefined; + private _collapsibleState: languages.CommentThreadCollapsibleState | undefined = languages.CommentThreadCollapsibleState.Expanded; get collapsibleState() { return this._collapsibleState; } diff --git a/src/vs/workbench/api/browser/mainThreadOutputService.ts b/src/vs/workbench/api/browser/mainThreadOutputService.ts index 2fa6443b374..4fff96c5150 100644 --- a/src/vs/workbench/api/browser/mainThreadOutputService.ts +++ b/src/vs/workbench/api/browser/mainThreadOutputService.ts @@ -8,10 +8,13 @@ import { Extensions, IOutputChannelRegistry, IOutputService, IOutputChannel, OUT import { MainThreadOutputServiceShape, MainContext, ExtHostOutputServiceShape, ExtHostContext } from '../common/extHost.protocol.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { UriComponents, URI } from '../../../base/common/uri.js'; -import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { Event } from '../../../base/common/event.js'; import { IViewsService } from '../../services/views/common/viewsService.js'; import { isNumber } from '../../../base/common/types.js'; +import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../services/statusbar/browser/statusbar.js'; +import { localize } from '../../../nls.js'; @extHostNamedCustomer(MainContext.MainThreadOutputService) export class MainThreadOutputService extends Disposable implements MainThreadOutputServiceShape { @@ -21,21 +24,30 @@ export class MainThreadOutputService extends Disposable implements MainThreadOut private readonly _proxy: ExtHostOutputServiceShape; private readonly _outputService: IOutputService; private readonly _viewsService: IViewsService; + private readonly _configurationService: IConfigurationService; + private readonly _statusbarService: IStatusbarService; + + private readonly _outputStatusItem = this._register(new MutableDisposable()); constructor( extHostContext: IExtHostContext, @IOutputService outputService: IOutputService, @IViewsService viewsService: IViewsService, + @IConfigurationService configurationService: IConfigurationService, + @IStatusbarService statusbarService: IStatusbarService, ) { super(); this._outputService = outputService; this._viewsService = viewsService; + this._configurationService = configurationService; + this._statusbarService = statusbarService; this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostOutputService); const setVisibleChannel = () => { const visibleChannel = this._viewsService.isViewVisible(OUTPUT_VIEW_ID) ? this._outputService.getActiveChannel() : undefined; this._proxy.$setVisibleChannel(visibleChannel ? visibleChannel.id : null); + this._outputStatusItem.value = undefined; }; this._register(Event.any(this._outputService.onActiveOutputChannel, Event.filter(this._viewsService.onDidChangeViewVisibility, ({ id }) => id === OUTPUT_VIEW_ID))(() => setVisibleChannel())); setVisibleChannel(); @@ -65,8 +77,39 @@ export class MainThreadOutputService extends Disposable implements MainThreadOut public async $reveal(channelId: string, preserveFocus: boolean): Promise { const channel = this._getChannel(channelId); - if (channel) { - this._outputService.showChannel(channel.id, preserveFocus); + if (!channel) { + return; + } + + const viewsToShowQuietly = this._configurationService.getValue | undefined>('workbench.view.showQuietly') ?? {}; + if (!this._viewsService.isViewVisible(OUTPUT_VIEW_ID) && viewsToShowQuietly[OUTPUT_VIEW_ID]) { + this._showChannelQuietly(channel); + return; + } + + this._outputService.showChannel(channel.id, preserveFocus); + } + + // Show status bar indicator + private _showChannelQuietly(channel: IOutputChannel) { + const statusProperties: IStatusbarEntry = { + name: localize('status.showOutput', "Show Output"), + text: '$(output)', + ariaLabel: localize('status.showOutputAria', "Show {0} Output Channel", channel.label), + command: `workbench.action.output.show.${channel.id}`, + tooltip: localize('status.showOutputTooltip', "Show {0} Output Channel", channel.label), + kind: 'prominent' + }; + + if (!this._outputStatusItem.value) { + this._outputStatusItem.value = this._statusbarService.addEntry( + statusProperties, + 'status.view.showQuietly', + StatusbarAlignment.RIGHT, + { id: 'status.notifications', alignment: StatusbarAlignment.LEFT } + ); + } else { + this._outputStatusItem.value.update(statusProperties); } } diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index a916634b8ce..5b22d1f069f 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -323,7 +323,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { } if (typeof features.actionButton !== 'undefined') { - this._actionButton.set(features.actionButton, undefined); + this._actionButton.set(features.actionButton ?? undefined, undefined); } if (typeof features.count !== 'undefined') { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index ae5441d23a7..36e385c3e6e 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1546,7 +1546,7 @@ export interface SCMProviderFeatures { count?: number; commitTemplate?: string; acceptInputCommand?: languages.Command; - actionButton?: SCMActionButtonDto; + actionButton?: SCMActionButtonDto | null; statusBarCommands?: ICommandDto[]; } diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 603ca6cc2db..74eca08c943 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -370,6 +370,9 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } set collapsibleState(newState: vscode.CommentThreadCollapsibleState) { + if (this._collapseState === newState) { + return; + } this._collapseState = newState; this.modifications.collapsibleState = newState; this._onDidUpdateCommentThread.fire(); diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 85e5cba5394..ae2930c763f 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -681,7 +681,7 @@ class ExtHostSourceControl implements vscode.SourceControl { enabled: actionButton.enabled } satisfies SCMActionButtonDto : undefined; - this.#proxy.$updateSourceControl(this.handle, { actionButton: actionButtonDto }); + this.#proxy.$updateSourceControl(this.handle, { actionButton: actionButtonDto ?? null }); } diff --git a/src/vs/workbench/api/node/proxyResolver.ts b/src/vs/workbench/api/node/proxyResolver.ts index 98f8979d995..a4e4952e34a 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -11,21 +11,20 @@ import { ExtHostExtensionService } from './extHostExtensionService.js'; import { URI } from '../../../base/common/uri.js'; import { ILogService, LogLevel as LogServiceLevel } from '../../../platform/log/common/log.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; -import { LogLevel, createHttpPatch, createProxyResolver, createTlsPatch, ProxySupportSetting, ProxyAgentParams, createNetPatch, loadSystemCertificates, ResolveProxyWithRequest, getOrLoadAdditionalCertificates, LookupProxyAuthorization } from '@vscode/proxy-agent'; +import { LogLevel, createHttpPatch, createProxyResolver, createTlsPatch, ProxySupportSetting, ProxyAgentParams, createNetPatch, loadSystemCertificates, ResolveProxyWithRequest } from '@vscode/proxy-agent'; import { AuthInfo } from '../../../platform/request/common/request.js'; import { DisposableStore } from '../../../base/common/lifecycle.js'; import { createRequire } from 'node:module'; import type * as undiciType from 'undici-types'; import type * as tlsType from 'tls'; -import type * as streamType from 'stream'; import { lookupKerberosAuthorization } from '../../../platform/request/node/requestService.js'; +import * as proxyAgent from '@vscode/proxy-agent'; const require = createRequire(import.meta.url); const http = require('http'); const https = require('https'); const tls: typeof tlsType = require('tls'); const net = require('net'); -const undici: typeof undiciType = require('undici'); const systemCertificatesV2Default = false; const useElectronFetchDefault = false; @@ -48,6 +47,7 @@ export function connectProxyResolver( getProxyURL: () => configProvider.getConfiguration('http').get('proxy'), getProxySupport: () => configProvider.getConfiguration('http').get('proxySupport') || 'off', getNoProxyConfig: () => configProvider.getConfiguration('http').get('noProxy') || [], + isAdditionalFetchSupportEnabled: () => configProvider.getConfiguration('http').get('fetchAdditionalSupport', true), addCertificatesV1: () => certSettingV1(configProvider), addCertificatesV2: () => certSettingV2(configProvider), log: extHostLogService, @@ -90,8 +90,10 @@ export function connectProxyResolver( env: process.env, }; const { resolveProxyWithRequest, resolveProxyURL } = createProxyResolver(params); + const target = (proxyAgent as any).default || proxyAgent; + target.resolveProxyURL = resolveProxyURL; - patchGlobalFetch(configProvider, mainThreadTelemetry, initData, resolveProxyURL, params.lookupProxyAuthorization!, getOrLoadAdditionalCertificates.bind(undefined, params), disposables); + patchGlobalFetch(params, configProvider, mainThreadTelemetry, initData, resolveProxyURL, disposables); const lookup = createPatchedModules(params, resolveProxyWithRequest); return configureModuleLoading(extensionService, lookup); @@ -109,11 +111,11 @@ const unsafeHeaders = [ 'set-cookie', ]; -function patchGlobalFetch(configProvider: ExtHostConfigProvider, mainThreadTelemetry: MainThreadTelemetryShape, initData: IExtensionHostInitData, resolveProxyURL: (url: string) => Promise, lookupProxyAuthorization: LookupProxyAuthorization, loadAdditionalCertificates: () => Promise, disposables: DisposableStore) { +function patchGlobalFetch(params: ProxyAgentParams, configProvider: ExtHostConfigProvider, mainThreadTelemetry: MainThreadTelemetryShape, initData: IExtensionHostInitData, resolveProxyURL: (url: string) => Promise, disposables: DisposableStore) { if (!(globalThis as any).__vscodeOriginalFetch) { const originalFetch = globalThis.fetch; (globalThis as any).__vscodeOriginalFetch = originalFetch; - const patchedFetch = patchFetch(originalFetch, configProvider, resolveProxyURL, lookupProxyAuthorization, loadAdditionalCertificates); + const patchedFetch = proxyAgent.createFetchPatch(params, originalFetch, resolveProxyURL); (globalThis as any).__vscodePatchedFetch = patchedFetch; let useElectronFetch = false; if (!initData.remote.isRemote) { @@ -149,7 +151,7 @@ function patchGlobalFetch(configProvider: ExtHostConfigProvider, mainThreadTelem recordFetchFeatureUse(mainThreadTelemetry, 'integrity'); } if (!useElectronFetch || isDataUrl || isBlobUrl || isManualRedirect || integrity) { - const response = await patchedFetch(input, init, urlString); + const response = await patchedFetch(input, init); monitorResponseProperties(mainThreadTelemetry, response, urlString); return response; } @@ -171,120 +173,6 @@ function patchGlobalFetch(configProvider: ExtHostConfigProvider, mainThreadTelem } } -function patchFetch(originalFetch: typeof globalThis.fetch, configProvider: ExtHostConfigProvider, resolveProxyURL: (url: string) => Promise, lookupProxyAuthorization: LookupProxyAuthorization, loadAdditionalCertificates: () => Promise) { - return async function patchedFetch(input: string | URL | Request, init?: RequestInit, urlString?: string) { - const config = configProvider.getConfiguration('http'); - const enabled = config.get('fetchAdditionalSupport'); - if (!enabled) { - return originalFetch(input, init); - } - const proxySupport = config.get('proxySupport') || 'off'; - const doResolveProxy = proxySupport === 'override' || proxySupport === 'fallback' || (proxySupport === 'on' && ((init as any)?.dispatcher) === undefined); - const addCerts = config.get('systemCertificates'); - if (!doResolveProxy && !addCerts) { - return originalFetch(input, init); - } - if (!urlString) { // for testing - urlString = typeof input === 'string' ? input : 'cache' in input ? input.url : input.toString(); - } - const proxyURL = doResolveProxy ? await resolveProxyURL(urlString) : undefined; - if (!proxyURL && !addCerts) { - return originalFetch(input, init); - } - const ca = addCerts ? [...tls.rootCertificates, ...await loadAdditionalCertificates()] : undefined; - const { allowH2, requestCA, proxyCA } = getAgentOptions(ca, init); - if (!proxyURL) { - const modifiedInit = { - ...init, - dispatcher: new undici.Agent({ - allowH2, - connect: { ca: requestCA }, - }) - }; - return originalFetch(input, modifiedInit); - } - - const state: Record = {}; - const proxyAuthorization = await lookupProxyAuthorization(proxyURL, undefined, state); - const modifiedInit = { - ...init, - dispatcher: new undici.ProxyAgent({ - uri: proxyURL, - allowH2, - headers: proxyAuthorization ? { 'Proxy-Authorization': proxyAuthorization } : undefined, - ...(requestCA ? { requestTls: { ca: requestCA } } : {}), - ...(proxyCA ? { proxyTls: { ca: proxyCA } } : {}), - clientFactory: (origin: URL, opts: object): undiciType.Dispatcher => (new undici.Pool(origin, opts) as any).compose((dispatch: undiciType.Dispatcher['dispatch']) => { - class ProxyAuthHandler extends undici.DecoratorHandler { - private abort: ((err?: Error) => void) | undefined; - constructor(private dispatch: undiciType.Dispatcher['dispatch'], private options: undiciType.Dispatcher.DispatchOptions, private handler: undiciType.Dispatcher.DispatchHandlers) { - super(handler); - } - onConnect(abort: (err?: Error) => void): void { - this.abort = abort; - this.handler.onConnect?.(abort); - } - onError(err: Error): void { - if (!(err instanceof ProxyAuthError)) { - return this.handler.onError?.(err); - } - (async () => { - try { - const proxyAuthorization = await lookupProxyAuthorization(proxyURL!, err.proxyAuthenticate, state); - if (proxyAuthorization) { - if (!this.options.headers) { - this.options.headers = ['Proxy-Authorization', proxyAuthorization]; - } else if (Array.isArray(this.options.headers)) { - const i = this.options.headers.findIndex((value, index) => index % 2 === 0 && value.toLowerCase() === 'proxy-authorization'); - if (i === -1) { - this.options.headers.push('Proxy-Authorization', proxyAuthorization); - } else { - this.options.headers[i + 1] = proxyAuthorization; - } - } else { - this.options.headers['Proxy-Authorization'] = proxyAuthorization; - } - this.dispatch(this.options, this); - } else { - this.handler.onError?.(new undici.errors.RequestAbortedError(`Proxy response (407) ?.== 200 when HTTP Tunneling`)); // Mimick undici's behavior - } - } catch (err) { - this.handler.onError?.(err); - } - })(); - } - onUpgrade(statusCode: number, headers: Buffer[] | string[] | null, socket: streamType.Duplex): void { - if (statusCode === 407 && headers) { - const proxyAuthenticate: string[] = []; - for (let i = 0; i < headers.length; i += 2) { - if (headers[i].toString().toLowerCase() === 'proxy-authenticate') { - proxyAuthenticate.push(headers[i + 1].toString()); - } - } - if (proxyAuthenticate.length) { - this.abort?.(new ProxyAuthError(proxyAuthenticate)); - return; - } - } - this.handler.onUpgrade?.(statusCode, headers, socket); - } - } - return function proxyAuthDispatch(options: undiciType.Dispatcher.DispatchOptions, handler: undiciType.Dispatcher.DispatchHandlers) { - return dispatch(options, new ProxyAuthHandler(dispatch, options, handler)); - }; - }), - }) - }; - return originalFetch(input, modifiedInit); - }; -} - -class ProxyAuthError extends Error { - constructor(public proxyAuthenticate: string[]) { - super('Proxy authentication required'); - } -} - function monitorResponseProperties(mainThreadTelemetry: MainThreadTelemetryShape, response: Response, urlString: string) { const originalUrl = response.url; Object.defineProperty(response, 'url', { @@ -348,7 +236,9 @@ function recordFetchFeatureUse(mainThreadTelemetry: MainThreadTelemetryShape, fe function createPatchedModules(params: ProxyAgentParams, resolveProxy: ResolveProxyWithRequest) { function mergeModules(module: any, patch: any) { - return Object.assign(module.default || module, patch); + const target = module.default || module; + target.__vscodeOriginal = Object.assign({}, target); + return Object.assign(target, patch); } return { @@ -396,7 +286,7 @@ function configureModuleLoading(extensionService: ExtHostExtensionService, looku if (!cache[request]) { if (request === 'undici') { const undici = original.apply(this, arguments); - patchUndici(undici); + proxyAgent.patchUndici(undici); cache[request] = undici; } else { const mod = lookup[request]; @@ -408,54 +298,6 @@ function configureModuleLoading(extensionService: ExtHostExtensionService, looku }); } -const agentOptions = Symbol('agentOptions'); -const proxyAgentOptions = Symbol('proxyAgentOptions'); - -function patchUndici(undici: typeof undiciType) { - const originalAgent = undici.Agent; - const patchedAgent = function PatchedAgent(opts?: undiciType.Agent.Options): undiciType.Agent { - const agent = new originalAgent(opts); - (agent as any)[agentOptions] = { - ...opts, - ...(opts?.connect && typeof opts?.connect === 'object' ? { connect: { ...opts.connect } } : undefined), - }; - return agent; - }; - patchedAgent.prototype = originalAgent.prototype; - (undici as any).Agent = patchedAgent; - - const originalProxyAgent = undici.ProxyAgent; - const patchedProxyAgent = function PatchedProxyAgent(opts: undiciType.ProxyAgent.Options | string): undiciType.ProxyAgent { - const proxyAgent = new originalProxyAgent(opts); - (proxyAgent as any)[proxyAgentOptions] = typeof opts === 'string' ? opts : { - ...opts, - ...(opts?.connect && typeof opts?.connect === 'object' ? { connect: { ...opts.connect } } : undefined), - }; - return proxyAgent; - }; - patchedProxyAgent.prototype = originalProxyAgent.prototype; - (undici as any).ProxyAgent = patchedProxyAgent; -} - -function getAgentOptions(systemCA: string[] | undefined, requestInit: RequestInit | undefined) { - let allowH2: boolean | undefined; - let requestCA: string | Buffer | Array | undefined = systemCA; - let proxyCA: string | Buffer | Array | undefined = systemCA; - const dispatcher: undiciType.Dispatcher = (requestInit as any)?.dispatcher; - const originalAgentOptions: undiciType.Agent.Options | undefined = dispatcher && (dispatcher as any)[agentOptions]; - if (originalAgentOptions) { - allowH2 = originalAgentOptions.allowH2; - requestCA = originalAgentOptions.connect && typeof originalAgentOptions.connect === 'object' && 'ca' in originalAgentOptions.connect && originalAgentOptions.connect.ca || systemCA; - } - const originalProxyAgentOptions: undiciType.ProxyAgent.Options | string | undefined = dispatcher && (dispatcher as any)[proxyAgentOptions]; - if (originalProxyAgentOptions && typeof originalProxyAgentOptions === 'object') { - allowH2 = originalProxyAgentOptions.allowH2; - requestCA = originalProxyAgentOptions.requestTls && 'ca' in originalProxyAgentOptions.requestTls && originalProxyAgentOptions.requestTls.ca || systemCA; - proxyCA = originalProxyAgentOptions.proxyTls && 'ca' in originalProxyAgentOptions.proxyTls && originalProxyAgentOptions.proxyTls.ca || systemCA; - } - return { allowH2, requestCA, proxyCA }; -} - async function lookupProxyAuthorization( extHostWorkspace: IExtHostWorkspaceProvider, extHostLogService: ILogService, diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index 2524f73353d..71337cabefe 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -14,7 +14,7 @@ import { IStorageService } from '../../../../platform/storage/common/storage.js' import { contrastBorder } from '../../../../platform/theme/common/colorRegistry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ActiveAuxiliaryContext, AuxiliaryBarFocusContext } from '../../../common/contextkeys.js'; -import { ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_TOP_ACTIVE_BORDER, ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER, ACTIVITY_BAR_TOP_FOREGROUND, ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_DRAG_AND_DROP_BORDER, PANEL_INACTIVE_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_BORDER, SIDE_BAR_FOREGROUND } from '../../../common/theme.js'; +import { ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_TOP_ACTIVE_BORDER, ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER, ACTIVITY_BAR_TOP_FOREGROUND, ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_DRAG_AND_DROP_BORDER, PANEL_INACTIVE_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_BORDER, SIDE_BAR_TITLE_BORDER, SIDE_BAR_FOREGROUND } from '../../../common/theme.js'; import { IViewDescriptorService } from '../../../common/views.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, Parts, Position } from '../../../services/layout/browser/layoutService.js'; @@ -101,6 +101,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { 'auxiliarybar', 'auxiliarybar', undefined, + SIDE_BAR_TITLE_BORDER, notificationService, storageService, contextMenuService, diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index fc483b4d326..32335eb82c3 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -92,6 +92,7 @@ export abstract class CompositePart extends Part { private readonly nameForTelemetry: string, private readonly compositeCSSClass: string, private readonly titleForegroundColor: string | undefined, + private readonly titleBorderColor: string | undefined, id: string, options: IPartOptions ) { @@ -436,6 +437,8 @@ export abstract class CompositePart extends Part { updateStyles: () => { titleLabel.style.color = $this.titleForegroundColor ? $this.getColor($this.titleForegroundColor) || '' : ''; + const borderColor = $this.titleBorderColor ? $this.getColor($this.titleBorderColor) : undefined; + parent.style.borderBottom = borderColor ? `1px solid ${borderColor}` : ''; } }; } diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 1c777faacaf..8c75858d981 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -43,7 +43,8 @@ import { SplitEditorToFirstGroupAction, SplitEditorToLastGroupAction, SplitEditorToLeftGroupAction, SplitEditorToNextGroupAction, SplitEditorToPreviousGroupAction, SplitEditorToRightGroupAction, NavigateForwardInEditsAction, NavigateBackwardsInEditsAction, NavigateForwardInNavigationsAction, NavigateBackwardsInNavigationsAction, NavigatePreviousInNavigationsAction, NavigatePreviousInEditsAction, NavigateToLastNavigationLocationAction, MaximizeGroupHideSidebarAction, MoveEditorToNewWindowAction, CopyEditorToNewindowAction, RestoreEditorsToMainWindowAction, ToggleMaximizeEditorGroupAction, MinimizeOtherGroupsHideSidebarAction, CopyEditorGroupToNewWindowAction, - MoveEditorGroupToNewWindowAction, NewEmptyEditorWindowAction + MoveEditorGroupToNewWindowAction, NewEmptyEditorWindowAction, + ToggleOvertypeInsertMode } from './editorActions.js'; import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_EDITOR_GROUP_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, @@ -171,6 +172,8 @@ quickAccessRegistry.registerQuickAccessProvider({ //#region Actions & Commands +registerAction2(ToggleOvertypeInsertMode); + registerAction2(ChangeLanguageAction); registerAction2(ChangeEOLAction); registerAction2(ChangeEncodingAction); diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 2deaa49417b..be79b3f0af2 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -38,6 +38,7 @@ import { ICommandActionTitle } from '../../../../platform/action/common/action.j import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { resolveCommandsContext } from './editorCommandsContext.js'; import { IListService } from '../../../../platform/list/browser/listService.js'; +import { InputMode } from '../../../../editor/common/inputMode.js'; class ExecuteCommandAction extends Action2 { @@ -2696,3 +2697,32 @@ export class NewEmptyEditorWindowAction extends Action2 { auxiliaryEditorPart.activeGroup.focus(); } } + +export class ToggleOvertypeInsertMode extends Action2 { + + constructor() { + super({ + id: 'editor.action.toggleOvertypeInsertMode', + title: { + ...localize2('toggleOvertypeInsertMode', "Toggle Overtype/Insert Mode"), + mnemonicTitle: localize({ key: 'mitoggleOvertypeInsertMode', comment: ['&& denotes a mnemonic'] }, "&&Toggle Overtype/Insert Mode"), + }, + metadata: { + description: localize2('toggleOvertypeMode.description', "Toggle between overtype and insert mode"), + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Insert, + mac: { primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.KeyO }, + }, + f1: true, + category: Categories.View + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const oldInputMode = InputMode.getInputMode(); + const newInputMode = oldInputMode === 'insert' ? 'overtype' : 'insert'; + InputMode.setInputMode(newInputMode); + } +} diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index acc013522c5..a1b9bd5215c 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -56,6 +56,7 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { TabFocus } from '../../../../editor/browser/config/tabFocus.js'; import { IEditorGroupsService, IEditorPart } from '../../../services/editor/common/editorGroupsService.js'; +import { InputMode } from '../../../../editor/common/inputMode.js'; class SideBySideEditorEncodingSupport implements IEncodingSupport { constructor(private primary: IEncodingSupport, private secondary: IEncodingSupport) { } @@ -149,6 +150,7 @@ class StateChange { encoding: boolean = false; EOL: boolean = false; tabFocusMode: boolean = false; + inputMode: boolean = false; columnSelectionMode: boolean = false; metadata: boolean = false; @@ -160,6 +162,7 @@ class StateChange { this.encoding = this.encoding || other.encoding; this.EOL = this.EOL || other.EOL; this.tabFocusMode = this.tabFocusMode || other.tabFocusMode; + this.inputMode = this.inputMode || other.inputMode; this.columnSelectionMode = this.columnSelectionMode || other.columnSelectionMode; this.metadata = this.metadata || other.metadata; } @@ -172,6 +175,7 @@ class StateChange { || this.encoding || this.EOL || this.tabFocusMode + || this.inputMode || this.columnSelectionMode || this.metadata; } @@ -186,6 +190,7 @@ type StateDelta = ( | { type: 'tabFocusMode'; tabFocusMode: boolean } | { type: 'columnSelectionMode'; columnSelectionMode: boolean } | { type: 'metadata'; metadata: string | undefined } + | { type: 'inputMode'; inputMode: 'overtype' | 'insert' } ); class State { @@ -208,6 +213,9 @@ class State { private _tabFocusMode: boolean | undefined; get tabFocusMode(): boolean | undefined { return this._tabFocusMode; } + private _inputMode: 'overtype' | 'insert' | undefined; + get inputMode(): 'overtype' | 'insert' | undefined { return this._inputMode; } + private _columnSelectionMode: boolean | undefined; get columnSelectionMode(): boolean | undefined { return this._columnSelectionMode; } @@ -260,6 +268,13 @@ class State { } break; + case 'inputMode': + if (this._inputMode !== update.inputMode) { + this._inputMode = update.inputMode; + change.inputMode = true; + } + break; + case 'columnSelectionMode': if (this._columnSelectionMode !== update.columnSelectionMode) { this._columnSelectionMode = update.columnSelectionMode; @@ -307,6 +322,18 @@ class TabFocusMode extends Disposable { } } +class StatusInputMode extends Disposable { + + private readonly _onDidChange = this._register(new Emitter<'overtype' | 'insert'>()); + public readonly onDidChange = this._onDidChange.event; + + constructor() { + super(); + InputMode.setInputMode('insert'); + this._register(InputMode.onDidChangeInputMode(inputMode => this._onDidChange.fire(inputMode))); + } +} + const nlsSingleSelectionRange = localize('singleSelectionRange', "Ln {0}, Col {1} ({2} selected)"); const nlsSingleSelection = localize('singleSelection', "Ln {0}, Col {1}"); const nlsMultiSelectionRange = localize('multiSelectionRange', "{0} selections ({1} characters selected)"); @@ -317,6 +344,7 @@ const nlsEOLCRLF = localize('endOfLineCarriageReturnLineFeed', "CRLF"); class EditorStatus extends Disposable { private readonly tabFocusModeElement = this._register(new MutableDisposable()); + private readonly inputModeElement = this._register(new MutableDisposable()); private readonly columnSelectionModeElement = this._register(new MutableDisposable()); private readonly indentationElement = this._register(new MutableDisposable()); private readonly selectionElement = this._register(new MutableDisposable()); @@ -327,6 +355,7 @@ class EditorStatus extends Disposable { private readonly currentMarkerStatus = this._register(this.instantiationService.createInstance(ShowCurrentMarkerInStatusbarContribution)); private readonly tabFocusMode = this._register(this.instantiationService.createInstance(TabFocusMode)); + private readonly inputMode = this._register(this.instantiationService.createInstance(StatusInputMode)); private readonly state = new State(); private toRender: StateChange | undefined = undefined; @@ -361,6 +390,7 @@ class EditorStatus extends Disposable { this.onTabFocusModeChange(this.configurationService.getValue('editor.tabFocusMode')); } })); + this._register(Event.runAndSubscribe(this.inputMode.onDidChange, (inputMode) => this.onInputModeChange(inputMode ?? 'insert'))); } private registerCommands(): void { @@ -422,6 +452,25 @@ class EditorStatus extends Disposable { } } + private updateInputModeElement(inputMode: 'overtype' | 'insert' | undefined): void { + if (inputMode === 'overtype') { + if (!this.inputModeElement.value) { + const text = localize('inputModeOvertype', 'OVR'); + const name = localize('status.editor.enableInsertMode', "Enable Insert Mode"); + this.inputModeElement.value = this.statusbarService.addEntry({ + name, + text, + ariaLabel: text, + tooltip: name, + command: 'editor.action.toggleOvertypeInsertMode', + kind: 'prominent' + }, 'status.editor.inputMode', StatusbarAlignment.RIGHT, 100.6); + } + } else { + this.inputModeElement.clear(); + } + } + private updateColumnSelectionModeElement(visible: boolean): void { if (visible) { if (!this.columnSelectionModeElement.value) { @@ -586,6 +635,7 @@ class EditorStatus extends Disposable { private doRenderNow(): void { this.updateTabFocusModeElement(!!this.state.tabFocusMode); + this.updateInputModeElement(this.state.inputMode); this.updateColumnSelectionModeElement(!!this.state.columnSelectionMode); this.updateIndentationElement(this.state.indentation); this.updateSelectionElement(this.state.selectionStatus); @@ -872,6 +922,11 @@ class EditorStatus extends Disposable { this.updateState(info); } + private onInputModeChange(inputMode: 'insert' | 'overtype'): void { + const info: StateDelta = { type: 'inputMode', inputMode }; + this.updateState(info); + } + private isActiveEditor(control: IEditorPane): boolean { const activeEditorPane = this.editorService.activeEditorPane; diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 1f2bd9ee9d5..854c02e6bd5 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -56,7 +56,6 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar { line-height: 27px; /* matches panel titles in settings */ - height: 35px; } .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item, diff --git a/src/vs/workbench/browser/parts/paneCompositePart.ts b/src/vs/workbench/browser/parts/paneCompositePart.ts index 413ce899f78..cfad7c02b95 100644 --- a/src/vs/workbench/browser/parts/paneCompositePart.ts +++ b/src/vs/workbench/browser/parts/paneCompositePart.ts @@ -142,6 +142,7 @@ export abstract class AbstractPaneCompositePart extends CompositePart(ConfigurationExtensions.Con 'default': false, 'description': localize('viewVisibility', "Controls the visibility of view header actions. View header actions may either be always visible, or only visible when that view is focused or hovered over.") }, + 'workbench.view.showQuietly': { + 'type': 'object', + 'description': localize('workbench.view.showQuietly', "If an extension requests a hidden view to be shown, display a clickable status bar indicator instead."), + 'scope': ConfigurationScope.WINDOW, + 'properties': { + 'workbench.panel.output': { + 'type': 'boolean', + 'description': localize('workbench.panel.output', "Output view") + } + }, + 'additionalProperties': false + }, 'workbench.fontAliasing': { 'type': 'string', 'enum': ['default', 'antialiased', 'none', 'auto'], diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index f8a88bd495d..5b0f4493d37 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -268,6 +268,13 @@ export const PANEL_BORDER = registerColor('panel.border', { hcLight: contrastBorder }, localize('panelBorder', "Panel border color to separate the panel from the editor. Panels are shown below the editor area and contain views like output and integrated terminal.")); +export const PANEL_TITLE_BORDER = registerColor('panelTitle.border', { + dark: null, + light: null, + hcDark: PANEL_BORDER, + hcLight: PANEL_BORDER +}, localize('panelTitleBorder', "Panel title border color on the bottom, separating the title from the views. Panels are shown below the editor area and contain views like output and integrated terminal.")); + export const PANEL_ACTIVE_TITLE_FOREGROUND = registerColor('panelTitle.activeForeground', { dark: '#E7E7E7', light: '#424242', @@ -612,6 +619,13 @@ export const SIDE_BAR_TITLE_BACKGROUND = registerColor('sideBarTitle.background' export const SIDE_BAR_TITLE_FOREGROUND = registerColor('sideBarTitle.foreground', SIDE_BAR_FOREGROUND, localize('sideBarTitleForeground', "Side bar title foreground color. The side bar is the container for views like explorer and search.")); +export const SIDE_BAR_TITLE_BORDER = registerColor('sideBarTitle.border', { + dark: null, + light: null, + hcDark: SIDE_BAR_BORDER, + hcLight: SIDE_BAR_BORDER +}, localize('sideBarTitleBorder', "Side bar title border color on the bottom, separating the title from the views. The side bar is the container for views like explorer and search.")); + export const SIDE_BAR_DRAG_AND_DROP_BACKGROUND = registerColor('sideBar.dropBackground', EDITOR_DRAG_AND_DROP_BACKGROUND, localize('sideBarDragAndDropBackground', "Drag and drop feedback color for the side bar sections. The color should have transparency so that the side bar sections can still shine through. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); export const SIDE_BAR_SECTION_HEADER_BACKGROUND = registerColor('sideBarSectionHeader.background', { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 09050b213f6..e29e1941e00 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -15,7 +15,7 @@ import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { EditorAction2 } from '../../../../../editor/browser/editorExtensions.js'; import { Position } from '../../../../../editor/common/core/position.js'; import { SuggestController } from '../../../../../editor/contrib/suggest/browser/suggestController.js'; -import { localize, localize2 } from '../../../../../nls.js'; +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'; @@ -32,6 +32,7 @@ import { IEditorGroupsService } from '../../../../services/editor/common/editorG import { ACTIVE_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; import { IHostService } from '../../../../services/host/browser/host.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; +import { EXTENSIONS_CATEGORY, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { extractAgentAndCommand } from '../../common/chatParserTypes.js'; @@ -39,6 +40,7 @@ import { IChatDetail, IChatService } from '../../common/chatService.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/chatViewModel.js'; import { IChatWidgetHistoryService } from '../../common/chatWidgetHistoryService.js'; +import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js'; import { ChatViewId, IChatWidget, IChatWidgetService, showChatView } from '../chat.js'; import { IChatEditorOptions } from '../chatEditor.js'; import { ChatEditorInput } from '../chatEditorInput.js'; @@ -86,7 +88,7 @@ class OpenChatGlobalAction extends Action2 { super({ id: CHAT_OPEN_ACTION_ID, title: OpenChatGlobalAction.TITLE, - icon: defaultChat.icon, + icon: Codicon.copilot, f1: true, precondition: ContextKeyExpr.or( ChatContextKeys.Setup.installed, @@ -460,31 +462,48 @@ export function registerChatActions() { } }); - registerAction2(class LearnMoreChatAction extends Action2 { + function registerOpenLinkAction(id: string, title: ILocalizedString, url: string, order: number): void { + registerAction2(class extends Action2 { + constructor() { + super({ + id, + title, + category: CHAT_CATEGORY, + f1: true, + precondition: ChatContextKeys.enabled, + menu: { + id: MenuId.ChatCommandCenter, + group: 'y_manage', + order + } + }); + } - static readonly ID = 'workbench.action.chat.learnMore'; - static readonly TITLE = localize2('learnMore', "Learn More"); + override async run(accessor: ServicesAccessor): Promise { + const openerService = accessor.get(IOpenerService); + openerService.open(URI.parse(url)); + } + }); + } + + registerOpenLinkAction('workbench.action.chat.managePlan', localize2('managePlan', "Manage Copilot Plan"), defaultChat.managePlanUrl, 1); + registerOpenLinkAction('workbench.action.chat.manageSettings', localize2('manageSettings', "Manage Copilot Settings"), defaultChat.manageSettingsUrl, 2); + registerOpenLinkAction('workbench.action.chat.learnMore', localize2('learnMore', "Learn More"), defaultChat.documentationUrl, 3); + + registerAction2(class ShowExtensionsUsingCopilit extends Action2 { constructor() { super({ - id: LearnMoreChatAction.ID, - title: LearnMoreChatAction.TITLE, - category: CHAT_CATEGORY, - menu: [ - { - id: MenuId.ChatCommandCenter, - group: 'z_end', - order: 1 - } - ] + id: 'workbench.action.chat.showExtensionsUsingCopilot', + title: localize2('showCopilotUsageExtensions', "Show Extensions using Copilot"), + f1: true, + category: EXTENSIONS_CATEGORY, }); } override async run(accessor: ServicesAccessor): Promise { - const openerService = accessor.get(IOpenerService); - if (defaultChat.documentationUrl) { - openerService.open(URI.parse(defaultChat.documentationUrl)); - } + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + extensionsWorkbenchService.openSearch(`@feature:${CopilotUsageExtensionFeatureId}`); } }); } @@ -501,15 +520,15 @@ export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewMod // --- command center chat const defaultChat = { - name: product.defaultChatAgent?.name ?? '', - icon: Codicon[product.defaultChatAgent?.icon as keyof typeof Codicon ?? 'commentDiscussion'], documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', + manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', + managePlanUrl: product.defaultChatAgent?.managePlanUrl ?? '', }; MenuRegistry.appendMenuItem(MenuId.CommandCenter, { submenu: MenuId.ChatCommandCenter, title: localize('title4', "Chat"), - icon: defaultChat.icon, + icon: Codicon.copilot, when: ContextKeyExpr.and( ContextKeyExpr.has('config.chat.commandCenter.enabled'), ContextKeyExpr.or( @@ -522,12 +541,12 @@ MenuRegistry.appendMenuItem(MenuId.CommandCenter, { order: 10001, }); -registerAction2(class ToggleChatControl extends ToggleTitleBarConfigAction { +registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction { constructor() { super( 'chat.commandCenter.enabled', - localize('toggle.chatControl', 'Chat Controls'), - localize('toggle.chatControlsDescription', "Toggle visibility of the Chat Controls in title bar"), 4, false, + localize('toggle.chatControl', 'Copilot Controls'), + localize('toggle.chatControlsDescription', "Toggle visibility of the Copilot Controls in title bar"), 4, false, ContextKeyExpr.and( ContextKeyExpr.has('config.window.commandCenter'), ContextKeyExpr.or( @@ -565,8 +584,8 @@ export class ChatCommandCenterRendering implements IWorkbenchContribution { const primaryAction = instantiationService.createInstance(MenuItemAction, { id: chatExtensionInstalled ? CHAT_OPEN_ACTION_ID : 'workbench.action.chat.triggerSetup', - title: chatExtensionInstalled ? OpenChatGlobalAction.TITLE : localize2('triggerChatSetup', "Use AI features with {0}...", defaultChat.name), - icon: defaultChat.icon, + title: chatExtensionInstalled ? OpenChatGlobalAction.TITLE : localize2('triggerChatSetup', "Use AI Features with Copilot for Free"), + icon: Codicon.copilot, }, undefined, undefined, undefined, undefined); return instantiationService.createInstance(DropdownWithPrimaryActionViewItem, primaryAction, dropdownAction, action.actions, '', { ...options, skipTelemetry: true }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index e67ca53cb16..f16ce19d66d 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -119,8 +119,15 @@ export function registerNewChatActions() { when: ContextKeyExpr.equals('view', EditsViewId), group: 'navigation', order: -1 - }, - ] + }], + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyL, + mac: { + primary: KeyMod.WinCtrl | KeyCode.KeyL + }, + when: ChatContextKeys.inChatSession + } }); } @@ -181,7 +188,7 @@ export function registerNewChatActions() { announceChatCleared(accessibilitySignalService); const widget = widgetService.getWidgetBySessionId(context.sessionId); if (widget) { - chatEditingService.currentEditingSessionObs.get()?.stop(); + chatEditingService.currentEditingSessionObs.get()?.stop(true); widget.clear(); widget.attachmentModel.clear(); widget.focusInput(); @@ -192,7 +199,7 @@ export function registerNewChatActions() { const widget = chatView.widget; announceChatCleared(accessibilitySignalService); - chatEditingService.currentEditingSessionObs.get()?.stop(); + chatEditingService.currentEditingSessionObs.get()?.stop(true); widget.clear(); widget.attachmentModel.clear(); widget.focusInput(); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 7244011a432..8880e5c4d59 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -517,7 +517,7 @@ export function registerChatCodeCompareBlockActions() { title: localize2('interactive.compare.apply', "Apply Edits"), f1: false, category: CHAT_CATEGORY, - icon: Codicon.check, + icon: Codicon.gitPullRequestGoToChanges, precondition: ContextKeyExpr.and(EditorContextKeys.hasChanges, ChatContextKeys.editApplied.negate()), menu: { id: MenuId.ChatCompareBlock, @@ -529,16 +529,10 @@ export function registerChatCodeCompareBlockActions() { async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise { - const editorService = accessor.get(IEditorService); const instaService = accessor.get(IInstantiationService); const editor = instaService.createInstance(DefaultChatTextEditor); - await editor.apply(context.element, context.edit, context.diffEditor); - - await editorService.openEditor({ - resource: context.edit.uri, - options: { revealIfVisible: true }, - }); + await editor.preview(context.element, context.edit); } }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index be164a6ec03..fe2e578e76c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -13,7 +13,7 @@ import { compare } from '../../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { IRange } from '../../../../../editor/common/core/range.js'; +import { IRange, Range } from '../../../../../editor/common/core/range.js'; import { EditorType } from '../../../../../editor/common/editorCommon.js'; import { Command } from '../../../../../editor/common/languages.js'; import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from '../../../../../editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.js'; @@ -250,26 +250,56 @@ class AttachSelectionToChatAction extends Action2 { title: localize2('workbench.action.chat.attachSelection.label', "Add Selection to Chat"), category: CHAT_CATEGORY, f1: false, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor')), - menu: { + precondition: ChatContextKeys.enabled, + menu: [{ id: MenuId.ChatCommandCenter, group: 'b_chat_context', + when: ActiveEditorContext.isEqualTo('workbench.editors.files.textFileEditor'), order: 15, - } + }, { + id: MenuId.SearchContext, + group: 'z_chat', + order: 2 + }] }); } override async run(accessor: ServicesAccessor, ...args: any[]): Promise { const variablesService = accessor.get(IChatVariablesService); const textEditorService = accessor.get(IEditorService); - - const activeEditor = textEditorService.activeTextEditorControl; - const activeUri = textEditorService.activeEditor?.resource; - if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { - const selection = activeEditor?.getSelection(); - if (selection) { - (await showChatView(accessor.get(IViewsService)))?.focusInput(); - variablesService.attachContext('file', { uri: activeUri, range: selection }, ChatAgentLocation.Panel); + const [_, matches] = args; + // If we have search matches, it means this is coming from the search widget + if (matches && matches.length > 0) { + const uris = new Map(); + for (const match of matches) { + if (isSearchTreeFileMatch(match)) { + uris.set(match.resource, undefined); + } else { + const context = { uri: match._parent.resource, range: match._range }; + const range = uris.get(context.uri); + if (!range || + range.startLineNumber !== context.range.startLineNumber && range.endLineNumber !== context.range.endLineNumber) { + uris.set(context.uri, context.range); + variablesService.attachContext('file', context, ChatAgentLocation.Panel); + } + } + } + // Add the root files for all of the ones that didn't have a match + for (const uri of uris) { + const [resource, range] = uri; + if (!range) { + variablesService.attachContext('file', { uri: resource }, ChatAgentLocation.Panel); + } + } + } else { + const activeEditor = textEditorService.activeTextEditorControl; + const activeUri = textEditorService.activeEditor?.resource; + if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { + const selection = activeEditor?.getSelection(); + if (selection) { + (await showChatView(accessor.get(IViewsService)))?.focusInput(); + variablesService.attachContext('file', { uri: activeUri, range: selection }, ChatAgentLocation.Panel); + } } } } @@ -407,6 +437,7 @@ export class AttachContextAction extends Action2 { kind: 'symbol', id: this._getFileContextId(pick.symbol.location), value: pick.symbol.location, + symbolKind: pick.symbol.kind, fullName: pick.label, name: pick.symbol.name, isDynamic: true diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 2e2159439ef..1b8f11d4851 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -279,7 +279,7 @@ class SendToChatEditingAction extends Action2 { return; } - await currentEditingSession?.stop(); + await currentEditingSession?.stop(true); } const { widget: editingWidget } = await viewsService.openView(EditsViewId) as ChatViewPane; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts index df44df50447..8dc7d4dee91 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts @@ -13,6 +13,9 @@ import { CHAT_OPEN_ACTION_ID } from './chatActions.js'; import { IExtensionManagementService, InstallOperation } from '../../../../../platform/extensionManagement/common/extensionManagement.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IDefaultChatAgent } from '../../../../../base/common/product.js'; +import { IViewDescriptorService } from '../../../../common/views.js'; +import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; +import { ensureSideBarChatViewSize } from '../chat.js'; export class ChatGettingStartedContribution extends Disposable implements IWorkbenchContribution { @@ -27,6 +30,8 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb @ICommandService private readonly commandService: ICommandService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IStorageService private readonly storageService: IStorageService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, ) { super(); @@ -56,6 +61,7 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb const extensionStatus = this.extensionService.getExtensionsStatus(); if (extensionStatus[ext.value].activationTimes && this.recentlyInstalled) { await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID); + ensureSideBarChatViewSize(400, this.viewDescriptorService, this.layoutService); this.storageService.store(ChatGettingStartedContribution.hideWelcomeView, true, StorageScope.APPLICATION, StorageTarget.MACHINE); this.recentlyInstalled = false; return; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index b1db925a3ff..4d7cc131b64 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -222,7 +222,6 @@ export function registerChatTitleActions() { } const chatService = accessor.get(IChatService); - const chatEditingService = accessor.get(IChatEditingService); const chatModel = chatService.getSession(item.sessionId); const chatRequests = chatModel?.getRequests(); if (!chatRequests) { @@ -232,9 +231,14 @@ export function registerChatTitleActions() { if (chatModel?.initialLocation === ChatAgentLocation.EditingSession) { const configurationService = accessor.get(IConfigurationService); const dialogService = accessor.get(IDialogService); + const chatEditingService = accessor.get(IChatEditingService); + const currentEditingSession = chatEditingService.currentEditingSession; + if (!currentEditingSession) { + return; + } // Prompt if the last request modified the working set and the user hasn't already disabled the dialog - const entriesModifiedInLastRequest = chatEditingService.currentEditingSessionObs.get()?.entries.get().filter((entry) => entry.lastModifyingRequestId === item.requestId) ?? []; + const entriesModifiedInLastRequest = currentEditingSession.entries.get().filter((entry) => entry.lastModifyingRequestId === item.requestId); const shouldPrompt = entriesModifiedInLastRequest.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRetry') === true; const confirmation = shouldPrompt ? await dialogService.confirm({ @@ -259,7 +263,7 @@ export function registerChatTitleActions() { // Reset the snapshot const snapshotRequest = chatRequests[itemIndex]; if (snapshotRequest) { - await chatEditingService.restoreSnapshot(snapshotRequest.id); + await currentEditingSession.restoreSnapshot(snapshotRequest.id); } } const request = chatModel?.getRequests().find(candidate => candidate.id === item.requestId); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 8900c8509a8..58ce2977a7b 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -60,7 +60,7 @@ import { registerChatEditorActions } from './chatEditorActions.js'; import { ChatEditorController } from './chatEditorController.js'; import { ChatEditorInput, ChatEditorInputSerializer } from './chatEditorInput.js'; import { ChatInputBoxContentProvider } from './chatEdinputInputContentProvider.js'; -import { ChatEditorSaving } from './chatEditorSaving.js'; +import { ChatEditorAutoSaveDisabler, ChatEditorSaving } from './chatEditorSaving.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './chatMarkdownDecorationsRenderer.js'; import { ChatCompatibilityNotifier, ChatExtensionPointHandler } from './chatParticipant.contribution.js'; import { ChatPasteProvidersFeature } from './chatPasteProviders.js'; @@ -80,7 +80,6 @@ import { ChatGettingStartedContribution } from './actions/chatGettingStarted.js' import { Extensions, IConfigurationMigrationRegistry } from '../../../common/configuration.js'; import { ChatEditorOverlayController } from './chatEditorOverlay.js'; import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js'; -import product from '../../../../platform/product/common/product.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -118,7 +117,7 @@ configurationRegistry.registerConfiguration({ 'chat.commandCenter.enabled': { type: 'boolean', tags: ['preview'], - markdownDescription: nls.localize('chat.commandCenter.enabled', "Controls whether the command center shows a menu for chat actions to control {0} (requires {1}).", product.defaultChatAgent?.chatName, '`#window.commandCenter#`'), + markdownDescription: nls.localize('chat.commandCenter.enabled', "Controls whether the command center shows a menu for actions to control Copilot (requires {0}).", '`#window.commandCenter#`'), default: true }, 'chat.experimental.offerSetup': { @@ -319,6 +318,7 @@ registerWorkbenchContribution2(ChatCommandCenterRendering.ID, ChatCommandCenterR registerWorkbenchContribution2(ChatImplicitContextContribution.ID, ChatImplicitContextContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatRelatedFilesContribution.ID, ChatRelatedFilesContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatEditorSaving.ID, ChatEditorSaving, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(ChatEditorAutoSaveDisabler.ID, ChatEditorAutoSaveDisabler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatViewsWelcomeHandler.ID, ChatViewsWelcomeHandler, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(ChatGettingStartedContribution.ID, ChatGettingStartedContribution, WorkbenchPhase.Eventually); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 9762b500e2e..fe43571e753 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -11,6 +11,8 @@ import { Selection } from '../../../../editor/common/core/selection.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; +import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData } from '../common/chatAgents.js'; import { IChatResponseModel } from '../common/chatModel.js'; @@ -50,6 +52,19 @@ export async function showEditsView(viewsService: IViewsService): Promise(EditsViewId))?.widget; } +export function ensureSideBarChatViewSize(width: number, viewDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): void { + const location = viewDescriptorService.getViewLocationById(ChatViewId); + if (location === ViewContainerLocation.Panel) { + return; // panel is typically very wide + } + + const viewPart = location === ViewContainerLocation.Sidebar ? Parts.SIDEBAR_PART : Parts.AUXILIARYBAR_PART; + const partSize = layoutService.getSize(viewPart); + if (partSize.width < width) { + layoutService.setSize(viewPart, { width: width, height: partSize.height }); + } +} + export const IQuickChatService = createDecorator('quickChatService'); export interface IQuickChatService { readonly _serviceBrand: undefined; @@ -111,6 +126,7 @@ export interface IChatListItemRendererOptions { readonly noPadding?: boolean; readonly editableCodeBlock?: boolean; readonly renderCodeBlockPills?: boolean; + readonly renderDetectedCommandsWithRequest?: boolean; readonly renderTextEditsAsSummary?: (uri: URI) => boolean; } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAgentCommandContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAgentCommandContentPart.ts new file mode 100644 index 00000000000..5f3e0a44fb7 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAgentCommandContentPart.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { IChatAgentCommand } from '../../common/chatAgents.js'; +import { chatSubcommandLeader } from '../../common/chatParserTypes.js'; +import { IChatRendererContent } from '../../common/chatViewModel.js'; +import { ChatTreeItem } from '../chat.js'; +import { IChatContentPart } from './chatContentParts.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { localize } from '../../../../../nls.js'; +import { Button } from '../../../../../base/browser/ui/button/button.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; + + +export class ChatAgentCommandContentPart extends Disposable implements IChatContentPart { + + readonly domNode: HTMLElement = document.createElement('span'); + + constructor( + cmd: IChatAgentCommand, + onClick: () => void, + @IHoverService private readonly _hoverService: IHoverService, + ) { + super(); + this.domNode.classList.add('chat-agent-command'); + this.domNode.setAttribute('aria-label', cmd.name); + this.domNode.setAttribute('role', 'button'); + + const groupId = generateUuid(); + + const commandSpan = document.createElement('span'); + this.domNode.appendChild(commandSpan); + commandSpan.innerText = chatSubcommandLeader + cmd.name; + this._store.add(this._hoverService.setupDelayedHover(commandSpan, { content: cmd.description, appearance: { showPointer: true } }, { groupId })); + + const rerun = localize('rerun', "Rerun without {0}{1}", chatSubcommandLeader, cmd.name); + const btn = new Button(this.domNode, { ariaLabel: rerun }); + btn.icon = Codicon.close; + this._store.add(btn.onDidClick(() => onClick())); + this._store.add(btn); + this._store.add(this._hoverService.setupDelayedHover(btn.element, { content: rerun, appearance: { showPointer: true } }, { groupId })); + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return false; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index 251f1e0c7dc..f4a329f0317 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -15,7 +15,7 @@ import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions import { IRange, Range } from '../../../../../editor/common/core/range.js'; import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; import { LanguageFeatureRegistry } from '../../../../../editor/common/languageFeatureRegistry.js'; -import { Location } from '../../../../../editor/common/languages.js'; +import { Location, SymbolKind } from '../../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; @@ -31,6 +31,7 @@ import { ITextEditorOptions } from '../../../../../platform/editor/common/editor import { FileKind, IFileService } from '../../../../../platform/files/common/files.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; import { IOpenerService, OpenInternalOptions } from '../../../../../platform/opener/common/opener.js'; import { FolderThemeIcon, IThemeService } from '../../../../../platform/theme/common/themeService.js'; import { fillEditorsDragData } from '../../../../browser/dnd.js'; @@ -61,6 +62,7 @@ export class ChatAttachmentsContentPart extends Disposable { @IFileService private readonly fileService: IFileService, @ICommandService private readonly commandService: ICommandService, @IThemeService private readonly themeService: IThemeService, + @ILabelService private readonly labelService: ILabelService ) { super(); @@ -74,9 +76,9 @@ export class ChatAttachmentsContentPart extends Disposable { const hoverDelegate = this.attachedContextDisposables.add(createInstantHoverDelegate()); this.variables.forEach(async (attachment) => { - const resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; - const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; - if (resource && attachment.isFile && this.workingSet.find(entry => entry.toString() === resource.toString())) { + let resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; + let range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; + if (resource && attachment.isFile && this.workingSet.find(entry => entry.toString() === resource?.toString())) { // Don't render attachment if it's in the working set return; } @@ -117,8 +119,11 @@ export class ChatAttachmentsContentPart extends Disposable { icon: !this.themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined }); - this.attachedContextDisposables.add(this.instantiationService.invokeFunction(accessor => hookUpResourceAttachmentDragAndContextMenu(accessor, widget, resource))); - + this.instantiationService.invokeFunction(accessor => { + if (resource) { + this.attachedContextDisposables.add(hookUpResourceAttachmentDragAndContextMenu(accessor, widget, resource)); + } + }); } else if (attachment.isImage) { ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name); @@ -152,21 +157,33 @@ export class ChatAttachmentsContentPart extends Disposable { } else if (isPasteVariableEntry(attachment)) { ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); - const hoverContent: IManagedHoverTooltipMarkdownString = { - markdown: { - value: `\`\`\`${attachment.language}\n${attachment.code}\n\`\`\``, - }, - markdownNotSupportedFallback: attachment.code, - }; - const classNames = ['file-icon', `${attachment.language}-lang-file-icon`]; - label.setLabel(attachment.fileName, undefined, { extraClasses: classNames }); + if (attachment.copiedFrom) { + resource = attachment.copiedFrom.uri; + range = attachment.copiedFrom.range; + const filename = basename(resource.path); + label.setLabel(filename, undefined, { extraClasses: classNames }); + } else { + label.setLabel(attachment.fileName, undefined, { extraClasses: classNames }); + } widget.appendChild(dom.$('span.attachment-additional-info', {}, `Pasted ${attachment.pastedLines}`)); widget.style.position = 'relative'; + const hoverContent: IManagedHoverTooltipMarkdownString = { + markdown: { + value: `**${attachment.copiedFrom ? this.labelService.getUriLabel(attachment.copiedFrom.uri, { relative: true }) : attachment.fileName}**\n\n---\n\n\`\`\`${attachment.language}\n${attachment.code}\n\`\`\``, + }, + markdownNotSupportedFallback: attachment.code, + }; + if (!this.attachedContextDisposables.isDisposed) { this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverContent, { trapFocus: true })); + + const resource = attachment.copiedFrom?.uri; + if (resource) { + this.attachedContextDisposables.add(this.instantiationService.invokeFunction(accessor => hookUpResourceAttachmentDragAndContextMenu(accessor, widget, resource))); + } } } else { const attachmentLabel = attachment.fullName ?? attachment.name; @@ -178,7 +195,7 @@ export class ChatAttachmentsContentPart extends Disposable { if (attachment.kind === 'symbol') { const scopedContextKeyService = this.attachedContextDisposables.add(this.contextKeyService.createScoped(widget)); - this.attachedContextDisposables.add(this.instantiationService.invokeFunction(accessor => hookUpSymbolAttachmentDragAndContextMenu(accessor, widget, scopedContextKeyService, attachment, MenuId.ChatInputSymbolAttachmentContext))); + this.attachedContextDisposables.add(this.instantiationService.invokeFunction(accessor => hookUpSymbolAttachmentDragAndContextMenu(accessor, widget, scopedContextKeyService, { ...attachment, kind: attachment.symbolKind }, MenuId.ChatInputSymbolAttachmentContext))); } if (isAttachmentPartialOrOmitted) { @@ -273,7 +290,7 @@ export function hookUpResourceAttachmentDragAndContextMenu(accessor: ServicesAcc return store; } -export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAccessor, widget: HTMLElement, scopedContextKeyService: IScopedContextKeyService, attachment: { name: string; value: Location }, contextMenuId: MenuId): IDisposable { +export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAccessor, widget: HTMLElement, scopedContextKeyService: IScopedContextKeyService, attachment: { name: string; value: Location; kind: SymbolKind }, contextMenuId: MenuId): IDisposable { const instantiationService = accessor.get(IInstantiationService); const languageFeaturesService = accessor.get(ILanguageFeaturesService); const textModelService = accessor.get(ITextModelService); @@ -295,7 +312,7 @@ export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAcces fsPath: attachment.value.uri.fsPath, range: attachment.value.range, name: attachment.name, - kind: 0 + kind: attachment.kind, }], e); e.dataTransfer?.setDragImage(widget, 0, 0); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index d4b7425654d..d0ac0fec701 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -326,7 +326,7 @@ class CollapsedCodeBlock extends Disposable { this._uri = uri; const iconText = this.labelService.getUriBasenameLabel(uri); - const modifiedEntry = this.chatEditingService.currentEditingSession?.entries.get().find(entry => entry.modifiedURI.toString() === uri.toString()); + const modifiedEntry = this.chatEditingService.currentEditingSession?.getEntry(uri); const isComplete = !modifiedEntry?.isCurrentlyBeingModified.get(); let iconClasses: string[] = []; diff --git a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts index 519b7c77373..96b99f953f6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts @@ -23,7 +23,7 @@ import { EditorInput } from '../../../common/editor/editorInput.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js'; -import { IChatRequestVariableEntry } from '../common/chatModel.js'; +import { IChatRequestVariableEntry, ISymbolVariableEntry } from '../common/chatModel.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { IChatInputStyles } from './chatInputPart.js'; @@ -285,13 +285,14 @@ export class ChatDragAndDrop extends Themable { return undefined; } - private resolveSymbolsAttachContext(symbols: DocumentSymbolTransferData[]): IChatRequestVariableEntry[] { + private resolveSymbolsAttachContext(symbols: DocumentSymbolTransferData[]): ISymbolVariableEntry[] { return symbols.map(symbol => { const resource = URI.file(symbol.fsPath); return { kind: 'symbol', id: symbolId(resource, symbol.range), value: { uri: resource, range: symbol.range }, + symbolKind: symbol.kind, fullName: `$(${SymbolKinds.toIcon(symbol.kind).id}) ${symbol.name}`, name: symbol.name, isDynamic: true diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index a2720ec691d..3c404e42f1f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -130,7 +130,7 @@ registerAction2(class OpenFileInDiffAction extends WorkingSetAction { async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, _chatWidget: IChatWidget, ...uris: URI[]): Promise { const editorService = accessor.get(IEditorService); for (const uri of uris) { - const editedFile = currentEditingSession.entries.get().find((e) => e.modifiedURI.toString() === uri.toString()); + const editedFile = currentEditingSession.getEntry(uri); if (editedFile?.state.get() === WorkingSetEntryState.Modified) { await editorService.openEditor({ original: { resource: URI.from(editedFile.originalURI, true) }, @@ -455,19 +455,26 @@ registerAction2(class RemoveAction extends Action2 { return; } + + + const configurationService = accessor.get(IConfigurationService); + const dialogService = accessor.get(IDialogService); + const chatEditingService = accessor.get(IChatEditingService); const chatService = accessor.get(IChatService); const chatModel = chatService.getSession(item.sessionId); if (chatModel?.initialLocation !== ChatAgentLocation.EditingSession) { return; } + const session = chatEditingService.currentEditingSession; + if (!session) { + return; + } + const requestId = isRequestVM(item) ? item.id : isResponseVM(item) ? item.requestId : undefined; if (requestId) { - const configurationService = accessor.get(IConfigurationService); - const dialogService = accessor.get(IDialogService); - const chatEditingService = accessor.get(IChatEditingService); const chatRequests = chatModel.getRequests(); const itemIndex = chatRequests.findIndex(request => request.id === requestId); const editsToUndo = chatRequests.length - itemIndex; @@ -514,7 +521,7 @@ registerAction2(class RemoveAction extends Action2 { // Restore the snapshot to what it was before the request(s) that we deleted const snapshotRequestId = chatRequests[itemIndex].id; - await chatEditingService.restoreSnapshot(snapshotRequestId); + await session.restoreSnapshot(snapshotRequestId); // Remove the request and all that come after it for (const request of requestsToRemove) { @@ -561,7 +568,7 @@ registerAction2(class OpenWorkingSetHistoryAction extends Action2 { } const snapshotRequestId = requests[snapshotRequestIndex]?.id; if (snapshotRequestId) { - const snapshot = chatEditingService.getSnapshotUri(snapshotRequestId, context.uri); + const snapshot = chatEditingService.currentEditingSession?.getSnapshotUri(snapshotRequestId, context.uri); if (snapshot) { const editor = await editorService.openEditor({ resource: snapshot, label: localize('chatEditing.snapshot', '{0} (Snapshot {1})', basename(context.uri), snapshotRequestIndex - 1), options: { transient: true, activation: EditorActivation.ACTIVATE } }); if (isCodeEditor(editor)) { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index defe157b7b7..2315db46e36 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -81,6 +81,11 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie return this._rewriteRatioObs; } + private readonly _maxLineNumberObs = observableValue(this, 0); + public get maxLineNumber(): IObservable { + return this._maxLineNumberObs; + } + private _isFirstEditAfterStartOrSnapshot: boolean = true; private _edit: OffsetEdit = OffsetEdit.empty; private _isEditFromUs: boolean = false; @@ -92,7 +97,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie return this._diffInfo; } - private readonly _editDecorationClear = this._register(new RunOnceScheduler(() => { this._editDecorations = this.doc.deltaDecorations(this._editDecorations, []); }, 3000)); + private readonly _editDecorationClear = this._register(new RunOnceScheduler(() => { this._editDecorations = this.doc.deltaDecorations(this._editDecorations, []); }, 500)); private _editDecorations: string[] = []; private static readonly _editDecorationOptions = ModelDecorationOptions.register({ @@ -192,6 +197,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie telemetryInfo: this._telemetryInfo }; } + restoreFromSnapshot(snapshot: ISnapshotEntry) { this._stateObs.set(snapshot.state, undefined); this.docSnapshot.setValue(snapshot.original); @@ -258,6 +264,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie } this._allEditsAreFromUs = false; + this._updateDiffInfoSeq(true); } if (!this.isCurrentlyBeingModified.get()) { @@ -271,8 +278,6 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie } } } - - this._updateDiffInfoSeq(!this._isEditFromUs); } acceptAgentEdits(textEdits: TextEdit[], isLastEdits: boolean): void { @@ -284,7 +289,6 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie range: edit.range } satisfies IModelDeltaDecoration; })); - this._editDecorationClear.schedule(); // push stack element for the first edit if (this._isFirstEditAfterStartOrSnapshot) { @@ -295,19 +299,21 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie } const ops = textEdits.map(TextEdit.asEditOperation); - this._applyEdits(ops); + const undoEdits = this._applyEdits(ops); transaction((tx) => { if (!isLastEdits) { this._stateObs.set(WorkingSetEntryState.Modified, tx); this._isCurrentlyBeingModifiedObs.set(true, tx); - const maxLineNumber = ops.reduce((max, op) => Math.max(max, op.range.endLineNumber), 0); + const maxLineNumber = undoEdits.reduce((max, op) => Math.max(max, op.range.startLineNumber), 0); const lineCount = this.doc.getLineCount(); this._rewriteRatioObs.set(Math.min(1, maxLineNumber / lineCount), tx); + this._maxLineNumberObs.set(maxLineNumber, tx); } else { this._resetEditsState(tx); this._updateDiffInfoSeq(true); this._rewriteRatioObs.set(1, tx); + this._editDecorationClear.schedule(); } }); } @@ -316,7 +322,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie // make the actual edit this._isEditFromUs = true; try { - this.doc.pushEditOperations(null, edits, () => null); + let result: ISingleEditOperation[] = []; + this.doc.pushEditOperations(null, edits, (undoEdits) => { + result = undoEdits; + return null; + }); + return result; } finally { this._isEditFromUs = false; } @@ -347,7 +358,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie { computeMoves: true, ignoreTrimWhitespace: false, maxComputationTimeMs: 3000 }, 'advanced' ), - timeout(fast ? 50 : 800) // DON't diff too fast + timeout(fast ? 0 : 800) // DON't diff too fast ]); if (this.docSnapshot.isDisposed() || this.doc.isDisposed()) { @@ -369,6 +380,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie } this.docSnapshot.setValue(this.doc.createSnapshot()); + this._diffInfo.set(nullDocumentDiff, transaction); this._edit = OffsetEdit.empty; this._stateObs.set(WorkingSetEntryState.Accepted, transaction); await this.collapse(transaction); @@ -381,9 +393,8 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie return; } - this._stateObs.set(WorkingSetEntryState.Rejected, transaction); - this._notifyAction('rejected'); if (this.createdInRequestId === this._telemetryInfo.requestId) { + await this.docFileEditorModel.revert({ soft: true }); await this._fileService.del(this.modifiedURI); this._onDidDelete.fire(); } else { @@ -395,6 +406,8 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie } await this.collapse(transaction); } + this._stateObs.set(WorkingSetEntryState.Rejected, transaction); + this._notifyAction('rejected'); } private _setDocValue(value: string): void { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts new file mode 100644 index 00000000000..2e9e5c1910d --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IReference } from '../../../../../base/common/lifecycle.js'; +import { ITransaction } from '../../../../../base/common/observable.js'; +import { ILanguageService } from '../../../../../editor/common/languages/language.js'; +import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; +import { IModelService } from '../../../../../editor/common/services/model.js'; +import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; +import { ChatEditKind } from '../../common/chatEditingService.js'; +import { IChatService } from '../../common/chatService.js'; +import { ChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo } from './chatEditingModifiedFileEntry.js'; + +export class ChatEditingModifiedNotebookEntry extends ChatEditingModifiedFileEntry { + constructor( + resourceRef: IReference, + _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, + _telemetryInfo: IModifiedEntryTelemetryInfo, + kind: ChatEditKind, + initialContent: string | undefined, + @IModelService modelService: IModelService, + @ITextModelService textModelService: ITextModelService, + @ILanguageService languageService: ILanguageService, + @IChatService _chatService: IChatService, + @IEditorWorkerService _editorWorkerService: IEditorWorkerService, + @IUndoRedoService _undoRedoService: IUndoRedoService, + @IFileService _fileService: IFileService, + ) { + super(resourceRef, _multiDiffEntryDelegate, _telemetryInfo, kind, initialContent, modelService, textModelService, languageService, _chatService, _editorWorkerService, _undoRedoService, _fileService); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index a4255e29f0d..ffde587ef1c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -14,6 +14,7 @@ import { ResourceMap } from '../../../../../base/common/map.js'; import { derived, IObservable, observableValue, runOnChange, ValueWithChangeEventFromObservable } from '../../../../../base/common/observable.js'; import { compare } from '../../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { isString } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; @@ -23,6 +24,7 @@ import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; import { IDecorationData, IDecorationsProvider, IDecorationsService } from '../../../../services/decorations/common/decorations.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; @@ -36,6 +38,9 @@ import { IChatService } from '../../common/chatService.js'; import { ChatEditingSession } from './chatEditingSession.js'; import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; + +const STORAGE_KEY_EDITING_SESSION = 'chat.editingSession'; + export class ChatEditingService extends Disposable implements IChatEditingService { _serviceBrand: undefined; @@ -70,6 +75,8 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return this._editingSessionFileLimit ?? defaultChatEditingMaxFileLimit; } + private _restoringEditingSession: Promise | undefined; + private _applyingChatEditsFailedContextKey: IContextKey; private _chatRelatedFilesProviders = new Map(); @@ -85,7 +92,8 @@ export class ChatEditingService extends Disposable implements IChatEditingServic @IDecorationsService decorationsService: IDecorationsService, @IFileService private readonly _fileService: IFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, - @IWorkbenchAssignmentService private readonly _workbenchAssignmentService: IWorkbenchAssignmentService + @IWorkbenchAssignmentService private readonly _workbenchAssignmentService: IWorkbenchAssignmentService, + @IStorageService storageService: IStorageService, ) { super(); this._applyingChatEditsFailedContextKey = applyingChatEditsFailedContextKey.bindTo(contextKeyService); @@ -142,6 +150,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic this._register(this.lifecycleService.onWillShutdown((e) => { const session = this._currentSessionObs.get(); if (session) { + storageService.store(STORAGE_KEY_EDITING_SESSION, session.chatSessionId, StorageScope.WORKSPACE, StorageTarget.MACHINE); e.join(session.storeState(), { id: 'join.chatEditingSession', label: localize('join.chatEditingSession', "Saving chat edits history") }); } })); @@ -151,28 +160,21 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return this._editingSessionFileLimit; }); void this._editingSessionFileLimitPromise; + + const sessionIdToRestore = storageService.get(STORAGE_KEY_EDITING_SESSION, StorageScope.WORKSPACE); + if (isString(sessionIdToRestore)) { + this._restoringEditingSession = this.startOrContinueEditingSession(sessionIdToRestore); + this._restoringEditingSession.finally(() => { + this._restoringEditingSession = undefined; + }); + } } - getSnapshotUri(id: string, uri: URI) { - const session = this._currentSessionObs.get(); - if (!session) { - return undefined; + async getOrRestoreEditingSession(): Promise { + if (this._restoringEditingSession) { + await this._restoringEditingSession; } - return session.getSnapshot(id, uri)?.snapshotUri; - } - - getEditingSession(resource: URI): IChatEditingSession | null { - const session = this.currentEditingSession; - if (!session) { - return null; - } - const entries = session.entries.get(); - for (const entry of entries) { - if (entry.modifiedURI.toString() === resource.toString()) { - return session; - } - } - return null; + return this.currentEditingSessionObs.get(); } override dispose(): void { @@ -181,17 +183,20 @@ export class ChatEditingService extends Disposable implements IChatEditingServic } async startOrContinueEditingSession(chatSessionId: string): Promise { + await this._restoringEditingSession; + const session = this._currentSessionObs.get(); if (session) { if (session.chatSessionId === chatSessionId) { return session; } else if (session.chatSessionId !== chatSessionId) { - await session.stop(); + await session.stop(true); } } return this._createEditingSession(chatSessionId); } + private async _createEditingSession(chatSessionId: string): Promise { if (this._currentSessionObs.get()) { throw new BugIndicatingError('Cannot have more than one active editing session'); @@ -200,12 +205,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic this._currentSessionDisposables.clear(); const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, this._editingSessionFileLimitPromise); + await session.init(); // listen for completed responses, run the code mapper and apply the edits to this edit session this._currentSessionDisposables.add(this.installAutoApplyObserver(session)); - await session.restoreState(); - this._currentSessionDisposables.add(session.onDidDispose(() => { this._currentSessionDisposables.clear(); this._currentSessionObs.set(null, undefined); @@ -221,17 +225,9 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return session; } - public createSnapshot(requestId: string): void { - this._currentSessionObs.get()?.createSnapshot(requestId); - } - - public async restoreSnapshot(requestId: string | undefined): Promise { - await this._currentSessionObs.get()?.restoreSnapshot(requestId); - } - private installAutoApplyObserver(session: ChatEditingSession): IDisposable { - const chatModel = this._chatService.getSession(session.chatSessionId); + const chatModel = this._chatService.getOrRestoreSession(session.chatSessionId); if (!chatModel) { throw new Error(`Edit session was created for a non-existing chat session: ${session.chatSessionId}`); } @@ -382,11 +378,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return undefined; } const userAddedWorkingSetEntries: URI[] = []; - for (const entry of currentSession.workingSet) { + for (const [uri, metadata] of currentSession.workingSet) { // Don't incorporate suggested files into the related files request // but do consider transient entries like open editors - if (entry[1].state !== WorkingSetEntryState.Suggested) { - userAddedWorkingSetEntries.push(entry[0]); + if (metadata.state !== WorkingSetEntryState.Suggested) { + userAddedWorkingSetEntries.push(uri); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index d82246020c1..b8a4aa82645 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -8,7 +8,7 @@ import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; -import { autorun, derived, IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { autorun, derived, IObservable, IReader, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js'; @@ -30,7 +30,7 @@ import { IEditorService } from '../../../../services/editor/common/editorService import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js'; import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; -import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, IChatEditingSession, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, IChatEditingSession, IModifiedFileEntry, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; import { ChatEditingMultiDiffSourceResolver } from './chatEditingService.js'; import { ChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; @@ -43,11 +43,14 @@ import { VSBuffer } from '../../../../../base/common/buffer.js'; import { IOffsetEdit, ISingleOffsetEdit, OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IChatService } from '../../common/chatService.js'; +import { INotebookService } from '../../../notebook/common/notebookService.js'; +import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js'; const STORAGE_CONTENTS_FOLDER = 'contents'; const STORAGE_STATE_FILE = 'state.json'; export class ChatEditingSession extends Disposable implements IChatEditingSession { + private readonly _state = observableValue(this, ChatEditingSessionState.Initial); private readonly _linearHistory = observableValue(this, []); private readonly _linearHistoryIndex = observableValue(this, 0); @@ -140,11 +143,27 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio @IFileService private readonly _fileService: IFileService, @IFileDialogService private readonly _dialogService: IFileDialogService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, - @IEnvironmentService private readonly _environmentService: IEnvironmentService, - @ILogService private readonly _logService: ILogService, @IChatService private readonly _chatService: IChatService, + @INotebookService private readonly _notebookService: INotebookService, ) { super(); + } + + public async init(): Promise { + const restoredSessionState = await this._instantiationService.createInstance(ChatEditingSessionStorage, this.chatSessionId).restoreState(); + if (restoredSessionState) { + for (const uri of restoredSessionState.filesToSkipCreating) { + this._filesToSkipCreating.add(uri); + } + for (const [uri, content] of restoredSessionState.initialFileContents) { + this._initialFileContents.set(uri, content); + } + this._pendingSnapshot = restoredSessionState.pendingSnapshot; + await this._restoreSnapshot(restoredSessionState.recentSnapshot); + this._linearHistoryIndex.set(restoredSessionState.linearHistoryIndex, undefined); + this._linearHistory.set(restoredSessionState.linearHistory, undefined); + this._state.set(ChatEditingSessionState.Idle, undefined); + } // Add the currently active editors to the working set this._trackCurrentEditorsInWorkingSet(); @@ -158,7 +177,27 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); })); + } + public getEntry(uri: URI): IModifiedFileEntry | undefined { + return this._entriesObs.get().find(e => isEqual(e.modifiedURI, uri)); + } + + public readEntry(uri: URI, reader: IReader | undefined): IModifiedFileEntry | undefined { + return this._entriesObs.read(reader).find(e => isEqual(e.modifiedURI, uri)); + } + + public storeState(): Promise { + const storage = this._instantiationService.createInstance(ChatEditingSessionStorage, this.chatSessionId); + const state: StoredSessionState = { + filesToSkipCreating: [...this._filesToSkipCreating], + initialFileContents: this._initialFileContents, + pendingSnapshot: this._pendingSnapshot, + recentSnapshot: this._createSnapshot(undefined), + linearHistoryIndex: this._linearHistoryIndex.get(), + linearHistory: this._linearHistory.get(), + }; + return storage.storeState(state); } private _trackCurrentEditorsInWorkingSet(e?: IEditorCloseEvent) { @@ -212,8 +251,10 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio public createSnapshot(requestId: string | undefined): void { const snapshot = this._createSnapshot(requestId); if (requestId) { - for (const workingSetItem of this._workingSet.keys()) { - this._workingSet.set(workingSetItem, { state: WorkingSetEntryState.Sent }); + for (const [uri, data] of this._workingSet) { + if (data.state !== WorkingSetEntryState.Suggested) { + this._workingSet.set(uri, { state: WorkingSetEntryState.Sent }); + } } const linearHistory = this._linearHistory.get(); const linearHistoryIndex = this._linearHistoryIndex.get(); @@ -258,12 +299,16 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return this._modelService.createModel(snapshotEntry.current, this._languageService.createById(snapshotEntry.languageId), snapshotUri, false); } - public getSnapshot(requestId: string, uri: URI) { + public getSnapshot(requestId: string, uri: URI): ISnapshotEntry | undefined { const snapshot = this._findSnapshot(requestId); const snapshotEntries = snapshot?.entries; return snapshotEntries?.get(uri); } + public getSnapshotUri(requestId: string, uri: URI): URI | undefined { + return this.getSnapshot(requestId, uri)?.snapshotUri; + } + /** * A snapshot representing the state of the working set before a new request has been sent */ @@ -404,11 +449,14 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio private stopPromise: Promise | undefined; - async stop(): Promise { + async stop(clearState = false): Promise { if (!this.stopPromise) { this.stopPromise = this._performStop(); } await this.stopPromise; + if (clearState) { + await this._instantiationService.createInstance(ChatEditingSessionStorage, this.chatSessionId).clearState(); + } } async _performStop(): Promise { @@ -423,9 +471,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }); })); - // delete the persisted editing session state - await this._clearState(); - if (this._state.get() !== ChatEditingSessionState.Disposed) { // session got disposed while we were closing editors and clearing state this.dispose(); @@ -602,6 +647,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const newEntries = this._entriesObs.get().filter(e => !isEqual(e.modifiedURI, entry.modifiedURI)); this._entriesObs.set(newEntries, undefined); this._workingSet.delete(entry.modifiedURI); + this._editorService.closeEditors(this._editorService.findEditors(entry.modifiedURI)); entry.dispose(); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); })); @@ -616,6 +662,9 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio try { const ref = await this._textModelService.createModelReference(resource); + if (this._notebookService.hasSupportedNotebooks(resource)) { + return this._instantiationService.createInstance(ChatEditingModifiedNotebookEntry, ref, { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }, responseModel, mustExist ? ChatEditKind.Created : ChatEditKind.Modified, initialContent); + } return this._instantiationService.createInstance(ChatEditingModifiedFileEntry, ref, { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }, responseModel, mustExist ? ChatEditKind.Created : ChatEditKind.Modified, initialContent); } catch (err) { if (mustExist) { @@ -637,13 +686,32 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio ?.collapsed.set(true, transaction); } } +} + +interface StoredSessionState { + readonly filesToSkipCreating: URI[]; + readonly initialFileContents: ResourceMap; + readonly pendingSnapshot?: IChatEditingSessionSnapshot; + readonly recentSnapshot: IChatEditingSessionSnapshot; + readonly linearHistoryIndex: number; + readonly linearHistory: IChatEditingSessionSnapshot[]; +} + +class ChatEditingSessionStorage { + constructor( + private readonly chatSessionId: string, + @IFileService private readonly _fileService: IFileService, + @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @ILogService private readonly _logService: ILogService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + ) { } private _getStorageLocation(): URI { const workspaceId = this._workspaceContextService.getWorkspace().id; return joinPath(this._environmentService.workspaceStorageHome, workspaceId, 'chatEditingSessions', this.chatSessionId); } - public async restoreState(): Promise { + public async restoreState(): Promise { const storageLocation = this._getStorageLocation(); const getFileContent = (hash: string) => { return this._fileService.readFile(joinPath(storageLocation, STORAGE_CONTENTS_FOLDER, hash)).then(content => content.value.toString()); @@ -682,39 +750,40 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const stateFilePath = joinPath(storageLocation, STORAGE_STATE_FILE); if (! await this._fileService.exists(stateFilePath)) { this._logService.debug(`chatEditingSession: No editing session state found at ${stateFilePath.toString()}`); - return false; + return undefined; } this._logService.debug(`chatEditingSession: Restoring editing session at ${stateFilePath.toString()}`); const stateFileContent = await this._fileService.readFile(stateFilePath); const data = JSON.parse(stateFileContent.value.toString()) as IChatEditingSessionDTO; if (data.version !== STORAGE_VERSION) { - return false; + return undefined; } - this._filesToSkipCreating.clear(); - this._initialFileContents.clear(); + const linearHistory = await Promise.all(data.linearHistory.map(deserializeChatEditingSessionSnapshot)); + const filesToSkipCreating = data.filesToSkipCreating.map((uriStr: string) => URI.parse(uriStr)); - const snapshotsFromHistory = await Promise.all(data.linearHistory.map(deserializeChatEditingSessionSnapshot)); - data.filesToSkipCreating.forEach((uriStr: string) => { - this._filesToSkipCreating.add(URI.parse(uriStr)); - }); + const initialFileContents = new ResourceMap(); for (const fileContentDTO of data.initialFileContents) { - this._initialFileContents.set(URI.parse(fileContentDTO[0]), await getFileContent(fileContentDTO[1])); + initialFileContents.set(URI.parse(fileContentDTO[0]), await getFileContent(fileContentDTO[1])); } - this._pendingSnapshot = data.pendingSnapshot ? await deserializeChatEditingSessionSnapshot(data.pendingSnapshot) : undefined; - this._restoreSnapshot(await deserializeChatEditingSessionSnapshot(data.recentSnapshot)); - this._linearHistoryIndex.set(data.linearHistoryIndex, undefined); - this._linearHistory.set(snapshotsFromHistory, undefined); - this._state.set(ChatEditingSessionState.Idle, undefined); - this._updateRequestHiddenState(); - return true; + const pendingSnapshot = data.pendingSnapshot ? await deserializeChatEditingSessionSnapshot(data.pendingSnapshot) : undefined; + const recentSnapshot = await deserializeChatEditingSessionSnapshot(data.recentSnapshot); + + return { + filesToSkipCreating, + initialFileContents, + pendingSnapshot, + recentSnapshot, + linearHistoryIndex: data.linearHistoryIndex, + linearHistory + }; } catch (e) { this._logService.error(`Error restoring chat editing session from ${storageLocation.toString()}`, e); } - return false; + return undefined; } - public async storeState(): Promise { + public async storeState(state: StoredSessionState): Promise { const storageFolder = this._getStorageLocation(); const contentsFolder = URI.joinPath(storageFolder, STORAGE_CONTENTS_FOLDER); @@ -774,12 +843,12 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const data = { version: STORAGE_VERSION, sessionId: this.chatSessionId, - linearHistory: this._linearHistory.get().map(serializeChatEditingSessionSnapshot), - linearHistoryIndex: this._linearHistoryIndex.get(), - initialFileContents: serializeResourceMap(this._initialFileContents, value => addFileContent(value)), - pendingSnapshot: this._pendingSnapshot ? serializeChatEditingSessionSnapshot(this._pendingSnapshot) : undefined, - recentSnapshot: serializeChatEditingSessionSnapshot(this._createSnapshot(undefined)), - filesToSkipCreating: Array.from(this._filesToSkipCreating.keys()).map(uri => uri.toString()), + linearHistory: state.linearHistory.map(serializeChatEditingSessionSnapshot), + linearHistoryIndex: state.linearHistoryIndex, + initialFileContents: serializeResourceMap(state.initialFileContents, value => addFileContent(value)), + pendingSnapshot: state.pendingSnapshot ? serializeChatEditingSessionSnapshot(state.pendingSnapshot) : undefined, + recentSnapshot: serializeChatEditingSessionSnapshot(state.recentSnapshot), + filesToSkipCreating: state.filesToSkipCreating.map(uri => uri.toString()), } satisfies IChatEditingSessionDTO; this._logService.debug(`chatEditingSession: Storing editing session at ${storageFolder.toString()}: ${fileContents.size} files`); @@ -790,21 +859,22 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio await this._fileService.writeFile(joinPath(storageFolder, STORAGE_STATE_FILE), VSBuffer.fromString(JSON.stringify(data, undefined, 2))); } catch (e) { - this._logService.error(`Error storing chat editing session to ${storageFolder.toString()}`, e); + this._logService.debug(`Error storing chat editing session to ${storageFolder.toString()}`, e); } } - private async _clearState(): Promise { + public async clearState(): Promise { const storageFolder = this._getStorageLocation(); if (await this._fileService.exists(storageFolder)) { this._logService.debug(`chatEditingSession: Clearing editing session at ${storageFolder.toString()}`); try { await this._fileService.del(storageFolder, { recursive: true }); } catch (e) { - this._logService.info(`Error clearing chat editing session from ${storageFolder.toString()}`, e); + this._logService.debug(`Error clearing chat editing session from ${storageFolder.toString()}`, e); } } } + } export interface IChatEditingSessionSnapshot { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index 16aa746bdd3..71fd23f68bd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -10,7 +10,7 @@ import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/c import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { CHAT_CATEGORY } from './actions/chatActions.js'; -import { ChatEditorController, ctxHasEditorModification } from './chatEditorController.js'; +import { ChatEditorController, ctxHasEditorModification, ctxHasRequestInProgress } from './chatEditorController.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js'; @@ -126,7 +126,7 @@ abstract class AcceptDiscardAction extends Action2 { ? localize2('accept2', 'Accept') : localize2('discard2', 'Discard'), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey, ContextKeyExpr.or(ctxHasEditorModification, ctxNotebookHasEditorModification)), + precondition: ContextKeyExpr.and(ctxHasRequestInProgress.negate(), hasUndecidedChatEditingResourceContextKey, ContextKeyExpr.or(ctxHasEditorModification, ctxNotebookHasEditorModification)), icon: accept ? Codicon.check : Codicon.discard, @@ -159,7 +159,7 @@ abstract class AcceptDiscardAction extends Action2 { return; } - const session = chatEditingService.getEditingSession(uri); + const session = chatEditingService.currentEditingSession; if (!session) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index 7ee423ec280..2b542a31157 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -7,7 +7,6 @@ import './media/chatEditorController.css'; import { getTotalWidth } from '../../../../base/browser/dom.js'; import { Disposable, DisposableStore, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; import { autorun, derived, IObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; -import { isEqual } from '../../../../base/common/resources.js'; import { themeColorFromId } from '../../../../base/common/themables.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, IViewZone, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; @@ -31,8 +30,10 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { Position } from '../../../../editor/common/core/position.js'; import { Selection } from '../../../../editor/common/core/selection.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; export const ctxHasEditorModification = new RawContextKey('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); +export const ctxHasRequestInProgress = new RawContextKey('chat.ctxHasRequestInProgress', false, localize('chat.ctxHasRequestInProgress', "The current editor shows a file from an edit session which is still in progress")); export class ChatEditorController extends Disposable implements IEditorContribution { @@ -47,6 +48,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut private _viewZones: string[] = []; private readonly _ctxHasEditorModification: IContextKey; + private readonly _ctxRequestInProgress: IContextKey; static get(editor: ICodeEditor): ChatEditorController | null { const controller = editor.getContribution(ChatEditorController.ID); @@ -56,6 +58,8 @@ export class ChatEditorController extends Disposable implements IEditorContribut private readonly _currentChange = observableValue(this, undefined); readonly currentChange: IObservable = this._currentChange; + private _scrollLock: boolean = false; + constructor( private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -66,13 +70,24 @@ export class ChatEditorController extends Disposable implements IEditorContribut super(); this._ctxHasEditorModification = ctxHasEditorModification.bindTo(contextKeyService); + this._ctxRequestInProgress = ctxHasRequestInProgress.bindTo(contextKeyService); - const configSignal = observableFromEvent( - Event.filter(this._editor.onDidChangeConfiguration, e => e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.lineHeight)), - _ => undefined - ); + const fontInfoObs = observableCodeEditor(this._editor).getOption(EditorOption.fontInfo); + const lineHeightObs = observableCodeEditor(this._editor).getOption(EditorOption.lineHeight); + const modelObs = observableCodeEditor(this._editor).model; - const modelObs = observableFromEvent(this._editor.onDidChangeModel, _ => this._editor.getModel()); + // scroll along unless "another" scroll happens + let ignoreScrollEvent = false; + this._store.add(this._editor.onDidScrollChange(e => { + if (e.scrollTopChanged && !ignoreScrollEvent) { + this._scrollLock = true; + } + })); + + this._store.add(autorun(r => { + const session = this._chatEditingService.currentEditingSessionObs.read(r); + this._ctxRequestInProgress.set(session?.state.read(r) === ChatEditingSessionState.StreamingEdits); + })); this._register(autorun(r => { @@ -81,23 +96,33 @@ export class ChatEditorController extends Disposable implements IEditorContribut return; } - configSignal.read(r); + fontInfoObs.read(r); + lineHeightObs.read(r); const model = modelObs.read(r); const session = this._chatEditingService.currentEditingSessionObs.read(r); - const entry = session?.entries.read(r).find(e => isEqual(e.modifiedURI, model?.uri)); + const entry = model?.uri ? session?.readEntry(model.uri, r) : undefined; if (!entry || entry.state.read(r) !== WorkingSetEntryState.Modified) { this._clearRendering(); return; } - const diff = entry?.diffInfo.read(r); - this._updateWithDiff(entry, diff); - this.initNavigation(); - if (this._currentChange.get() === undefined) { - this.revealNext(); + try { + ignoreScrollEvent = true; + if (!this._scrollLock) { + const maxLineNumber = entry.maxLineNumber.read(r); + this._editor.revealLineNearTop(maxLineNumber, ScrollType.Immediate); + } + const diff = entry?.diffInfo.read(r); + this._updateWithDiff(entry, diff); + this.initNavigation(); + if (this._currentChange.get() === undefined) { + this.revealNext(); + } + } finally { + ignoreScrollEvent = false; } })); @@ -106,7 +131,9 @@ export class ChatEditorController extends Disposable implements IEditorContribut if (!value || value.state.read(r) !== ChatEditingSessionState.StreamingEdits) { return false; } - return value.entries.read(r).some(e => isEqual(e.modifiedURI, this._editor.getModel()?.uri)); + + const model = modelObs.read(r); + return model ? value.readEntry(model.uri, r) : undefined; }); @@ -152,13 +179,11 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._diffVisualDecorations.clear(); this._diffLineDecorations.clear(); this._ctxHasEditorModification.reset(); + this._currentChange.set(undefined, undefined); + this._scrollLock = false; } - private _updateWithDiff(entry: IModifiedFileEntry, diff: IDocumentDiff | null | undefined): void { - if (!diff) { - this._clearRendering(); - return; - } + private _updateWithDiff(entry: IModifiedFileEntry, diff: IDocumentDiff): void { this._ctxHasEditorModification.set(true); const originalModel = entry.originalModel; @@ -371,6 +396,10 @@ export class ChatEditorController extends Disposable implements IEditorContribut })); } + unlockScroll(): void { + this._scrollLock = false; + } + initNavigation(): void { const position = this._editor.getPosition(); const range = position && this._diffLineDecorations.getRanges().find(r => r.containsPosition(position)); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index fd58d52bb5f..68a2751e37d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -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 { getWindow, reset, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js'; +import { EventLike, getWindow, reset, scheduleAtNextAnimationFrame } 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'; @@ -109,6 +109,10 @@ class ChatEditorOverlayWidget implements IOverlayWidget { return localize('tooltip_nm', "{0} changes in {1} files", changeCount, entriesCount); } } + + override onClick(event: EventLike, preserveFocus?: boolean): void { + ChatEditorController.get(that._editor)?.unlockScroll(); + } }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts index 4d0728bc076..0970f7ae916 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts @@ -8,10 +8,9 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { CancellationError } from '../../../../base/common/errors.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { Disposable, DisposableMap, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; -import { autorunWithStore } from '../../../../base/common/observable.js'; -import { isEqual } from '../../../../base/common/resources.js'; +import { autorun, autorunWithStore } from '../../../../base/common/observable.js'; import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; @@ -22,26 +21,97 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IEditorIdentifier, SaveReason } from '../../../common/editor.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; +import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; +import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; import { IChatModel } from '../common/chatModel.js'; import { IChatService } from '../common/chatService.js'; import { ChatEditingModifiedFileEntry } from './chatEditing/chatEditingModifiedFileEntry.js'; + +const STORAGE_KEY_AUTOSAVE_DISABLED = 'chat.editing.autosaveDisabled'; + +export class ChatEditorAutoSaveDisabler extends Disposable implements IWorkbenchContribution { + + static readonly ID: string = 'workbench.chat.autoSaveDisabler'; + + private _autosaveDisabledUris: string[] = []; + + constructor( + @IConfigurationService configService: IConfigurationService, + @IChatEditingService chatEditingService: IChatEditingService, + @IFilesConfigurationService fileConfigService: IFilesConfigurationService, + @ILifecycleService lifecycleService: ILifecycleService, + @IStorageService storageService: IStorageService + ) { + super(); + + // on shutdown remember all files that have auto save disabled + this._store.add(lifecycleService.onWillShutdown((e) => { + storageService.store(STORAGE_KEY_AUTOSAVE_DISABLED, this._autosaveDisabledUris, StorageScope.WORKSPACE, StorageTarget.MACHINE); + })); + + const alwaysSaveConfig = observableConfigValue(ChatEditorSaving._config, false, configService); + + // as quickly as possible disable auto save for all files that were modified before the last shutdown + if (!alwaysSaveConfig.get()) { + const autoSaveDisabled = storageService.getObject(STORAGE_KEY_AUTOSAVE_DISABLED, StorageScope.WORKSPACE, []); + if (Array.isArray(autoSaveDisabled) && autoSaveDisabled.length > 0) { + + const initializingStore = new DisposableStore(); + for (const uriString of autoSaveDisabled) { + initializingStore.add(fileConfigService.disableAutoSave(URI.parse(uriString))); + } + chatEditingService.getOrRestoreEditingSession().finally(() => { + // by now the session is restored and the auto save handlers are in place + initializingStore.dispose(); + }); + + } + } + + // listen to session changes and update auto save settings accordingly + const saveConfig = this._store.add(new MutableDisposable()); + this._store.add(autorun(reader => { + const store = new DisposableStore(); + const autoSaveDisabled: string[] = []; + try { + if (alwaysSaveConfig.read(reader)) { + return; + } + const session = chatEditingService.currentEditingSessionObs.read(reader); + if (session) { + const entries = session.entries.read(reader); + for (const entry of entries) { + if (entry.state.read(reader) === WorkingSetEntryState.Modified) { + autoSaveDisabled.push(entry.modifiedURI.toString()); + store.add(fileConfigService.disableAutoSave(entry.modifiedURI)); + } + } + } + } finally { + saveConfig.value = store; // disposes the previous store, after we have added the new one + this._autosaveDisabledUris = autoSaveDisabled; + } + })); + } +} + + export class ChatEditorSaving extends Disposable implements IWorkbenchContribution { static readonly ID: string = 'workbench.chat.editorSaving'; static readonly _config = 'chat.editing.alwaysSaveWithGeneratedChanges'; - private readonly _sessionStore = this._store.add(new DisposableMap()); - constructor( @IConfigurationService configService: IConfigurationService, @IChatEditingService chatEditingService: IChatEditingService, @@ -50,7 +120,6 @@ export class ChatEditorSaving extends Disposable implements IWorkbenchContributi @ILabelService labelService: ILabelService, @IDialogService dialogService: IDialogService, @IChatService private readonly _chatService: IChatService, - @IFilesConfigurationService private readonly _fileConfigService: IFilesConfigurationService, ) { super(); @@ -64,10 +133,8 @@ export class ChatEditorSaving extends Disposable implements IWorkbenchContributi if (!chatSession) { return; } - const entries = session.entries.read(r); - store.add(textFileService.files.onDidSave(e => { - const entry = entries.find(entry => isEqual(entry.modifiedURI, e.model.resource)); + const entry = session.getEntry(e.model.resource); if (entry && entry.state.get() === WorkingSetEntryState.Modified) { this._reportSavedWhenReady(chatSession, entry); } @@ -85,10 +152,6 @@ export class ChatEditorSaving extends Disposable implements IWorkbenchContributi return; } - if (chatEditingService.currentEditingSession) { - this._handleNewEditingSession(chatEditingService.currentEditingSession, store); - } - const saveJobs = new class { private _deferred?: DeferredPromise; @@ -151,7 +214,6 @@ export class ChatEditorSaving extends Disposable implements IWorkbenchContributi } }; - store.add(chatEditingService.onDidCreateEditingSession(e => this._handleNewEditingSession(e, store))); store.add(textFileService.files.addSaveParticipant({ participate: async (workingCopy, context, progress, token) => { @@ -161,16 +223,16 @@ export class ChatEditorSaving extends Disposable implements IWorkbenchContributi return; } - const session = chatEditingService.getEditingSession(workingCopy.resource); + const session = await chatEditingService.getOrRestoreEditingSession(); if (!session) { return; } - - if (!session.entries.get().find(e => e.state.get() === WorkingSetEntryState.Modified && e.modifiedURI.toString() === workingCopy.resource.toString())) { + const entry = session.getEntry(workingCopy.resource); + if (!entry || entry.state.get() !== WorkingSetEntryState.Modified) { return; } - return saveJobs.add(workingCopy.resource); + return saveJobs.add(entry.modifiedURI); } })); }; @@ -211,38 +273,6 @@ export class ChatEditorSaving extends Disposable implements IWorkbenchContributi }); this._store.add(d); } - - private _handleNewEditingSession(session: IChatEditingSession, container: DisposableStore) { - - const store = new DisposableStore(); - container.add(store); - - // disable auto save for those files involved in editing - const saveConfig = store.add(new MutableDisposable()); - const update = () => { - const store = new DisposableStore(); - const entries = session.entries.get(); - for (const entry of entries) { - if (entry.state.get() === WorkingSetEntryState.Modified) { - store.add(this._fileConfigService.disableAutoSave(entry.modifiedURI)); - } - } - saveConfig.value = store; - }; - - update(); - - this._sessionStore.set(session, store); - - store.add(session.onDidChange(() => { - update(); - })); - - store.add(session.onDidDispose(() => { - store.dispose(); - container.delete(store); - })); - } } export class ChatEditingSaveAllAction extends Action2 { diff --git a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts index e38cd46e305..e74cab4ab0c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts @@ -123,7 +123,7 @@ export class InlineAnchorWidget extends Disposable { }); })); - this._store.add(instantiationService.invokeFunction(accessor => hookUpSymbolAttachmentDragAndContextMenu(accessor, element, contextKeyService, { value: symbol.location, name: symbol.name }, MenuId.ChatInlineSymbolAnchorContext))); + this._store.add(instantiationService.invokeFunction(accessor => hookUpSymbolAttachmentDragAndContextMenu(accessor, element, contextKeyService, { value: symbol.location, name: symbol.name, kind: symbol.kind }, MenuId.ChatInlineSymbolAnchorContext))); } else { location = this.data; diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 3f3fea37900..7fa96ecb7e7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -13,7 +13,7 @@ import { Button } from '../../../../base/browser/ui/button/button.js'; import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { getBaseLayerHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate2.js'; -import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js'; @@ -24,7 +24,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { HistoryNavigator2 } from '../../../../base/common/history.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; import { basename, dirname } from '../../../../base/common/path.js'; import { isMacintosh } from '../../../../base/common/platform.js'; @@ -37,10 +37,13 @@ import { IDimension } from '../../../../editor/common/core/dimension.js'; import { IPosition } from '../../../../editor/common/core/position.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; import { isLocation } from '../../../../editor/common/languages.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { ITextModel } from '../../../../editor/common/model.js'; +import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { CopyPasteController } from '../../../../editor/contrib/dropOrPasteInto/browser/copyPasteController.js'; +import { DropIntoEditorController } from '../../../../editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.js'; import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js'; import { GlyphHoverController } from '../../../../editor/contrib/hover/browser/glyphHoverController.js'; import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js'; @@ -62,6 +65,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 { ILabelService } from '../../../../platform/label/common/label.js'; import { WorkbenchList } from '../../../../platform/list/browser/listService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; @@ -76,7 +80,7 @@ import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEd import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { ChatEditingSessionState, IChatEditingService, IChatEditingSession, WorkingSetEntryState } from '../common/chatEditingService.js'; +import { ChatEditingSessionState, IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js'; import { IChatRequestVariableEntry, isPasteVariableEntry } from '../common/chatModel.js'; import { ChatRequestDynamicVariablePart } from '../common/chatParserTypes.js'; import { IChatFollowup } from '../common/chatService.js'; @@ -265,6 +269,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge getContribsInputState: () => any, @IChatWidgetHistoryService private readonly historyService: IChatWidgetHistoryService, @IModelService private readonly modelService: IModelService, + @ILanguageService private readonly languageService: ILanguageService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -281,6 +286,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @IThemeService private readonly themeService: IThemeService, @ITextModelService private readonly textModelResolverService: ITextModelService, @IStorageService private readonly storageService: IStorageService, + @ILabelService private readonly labelService: ILabelService, ) { super(); @@ -652,6 +658,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._onDidBlur.fire(); })); + this._register(this._inputEditor.onDidBlurEditorWidget(() => { + CopyPasteController.get(this._inputEditor)?.clearWidgets(); + DropIntoEditorController.get(this._inputEditor)?.clearWidgets(); + })); const hoverDelegate = this._register(createInstantHoverDelegate()); @@ -799,8 +809,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge let ariaLabel: string | undefined; - const resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; - const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; + let resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; + let range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; if (resource && (attachment.isFile || attachment.isDirectory)) { const fileBasename = basename(resource.path); const fileDirname = dirname(resource.path); @@ -820,7 +830,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }); this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); - store.add(this.instantiationService.invokeFunction(accessor => hookUpResourceAttachmentDragAndContextMenu(accessor, widget, resource))); + this.instantiationService.invokeFunction(accessor => { + if (resource) { + store.add(hookUpResourceAttachmentDragAndContextMenu(accessor, widget, resource)); + } + }); } else if (attachment.isImage) { ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name); @@ -859,20 +873,33 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } else if (isPasteVariableEntry(attachment)) { ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); - const hoverContent: IManagedHoverTooltipMarkdownString = { - markdown: { - value: `\`\`\`${attachment.language}\n${attachment.code}\n\`\`\``, - }, - markdownNotSupportedFallback: attachment.code, - }; - const classNames = ['file-icon', `${attachment.language}-lang-file-icon`]; - label.setLabel(attachment.fileName, undefined, { extraClasses: classNames }); + if (attachment.copiedFrom) { + resource = attachment.copiedFrom.uri; + range = attachment.copiedFrom.range; + const filename = basename(resource.path); + label.setLabel(filename, undefined, { extraClasses: classNames }); + } else { + label.setLabel(attachment.fileName, undefined, { extraClasses: classNames }); + } widget.appendChild(dom.$('span.attachment-additional-info', {}, `Pasted ${attachment.pastedLines}`)); widget.style.position = 'relative'; + + const hoverContent: IManagedHoverTooltipMarkdownString = { + markdown: { + value: `**${attachment.copiedFrom ? this.labelService.getUriLabel(attachment.copiedFrom.uri, { relative: true }) : attachment.fileName}**\n\n---\n\n\`\`\`${attachment.language}\n${attachment.code}\n\`\`\``, + }, + markdownNotSupportedFallback: attachment.code, + }; store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverContent, { trapFocus: true })); this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); + + const copiedFromResource = attachment.copiedFrom?.uri; + if (copiedFromResource) { + store.add(this.instantiationService.invokeFunction(accessor => hookUpResourceAttachmentDragAndContextMenu(accessor, widget, copiedFromResource))); + } + } else { const attachmentLabel = attachment.fullName ?? attachment.name; const withIcon = attachment.icon?.id ? `$(${attachment.icon.id}) ${attachmentLabel}` : attachmentLabel; @@ -885,7 +912,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (attachment.kind === 'symbol') { const scopedContextKeyService = store.add(this.contextKeyService.createScoped(widget)); - store.add(this.instantiationService.invokeFunction(accessor => hookUpSymbolAttachmentDragAndContextMenu(accessor, widget, scopedContextKeyService, attachment, MenuId.ChatInputSymbolAttachmentContext))); + store.add(this.instantiationService.invokeFunction(accessor => hookUpSymbolAttachmentDragAndContextMenu(accessor, widget, scopedContextKeyService, { ...attachment, kind: attachment.symbolKind }, MenuId.ChatInputSymbolAttachmentContext))); } await Promise.all(attachmentInitPromises); @@ -1042,12 +1069,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge seenEntries.add(attachment.value); } } - for (const [file, state] of chatEditingSession.workingSet.entries()) { - if (!seenEntries.has(file)) { + for (const [file, metadata] of chatEditingSession.workingSet.entries()) { + if (!seenEntries.has(file) && metadata.state !== WorkingSetEntryState.Suggested) { entries.unshift({ reference: file, - state: state.state, - description: state.description, + state: metadata.state, + description: metadata.description, kind: 'reference', }); seenEntries.add(file); @@ -1205,7 +1232,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (e.element?.kind === 'reference' && URI.isUri(e.element.reference)) { const modifiedFileUri = e.element.reference; - const entry = chatEditingSession.entries.get().find(entry => entry.modifiedURI.toString() === modifiedFileUri.toString()); + const entry = chatEditingSession.getEntry(modifiedFileUri); const diffInfo = entry?.diffInfo.get(); const range = diffInfo?.changes.at(0)?.modified.toExclusiveRange(); @@ -1239,9 +1266,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const addFilesElement = innerContainer.querySelector('.chat-editing-session-toolbar-actions') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-toolbar-actions')); + const hoverDelegate = getDefaultHoverDelegate('element'); + const button = this._chatEditsActionsDisposables.add(new Button(addFilesElement, { supportIcons: true, - secondary: true + secondary: true, + hoverDelegate })); // Disable the button if the entries that are not suggested exceed the budget button.enabled = remainingFileEntriesBudget > 0; @@ -1251,6 +1281,54 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.commandService.executeCommand('workbench.action.chat.editing.attachFiles', { widget: chatWidget }); })); dom.append(addFilesElement, button.element); + + // REALTED files (after Add Files...) + for (const [uri, metadata] of chatEditingSession.workingSet) { + if (metadata.state !== WorkingSetEntryState.Suggested) { + continue; + } + const addBtn = this._chatEditsActionsDisposables.add(new Button(addFilesElement, { + supportIcons: true, + secondary: true, + hoverDelegate + })); + addBtn.enabled = remainingFileEntriesBudget > 0; + addBtn.label = this.labelService.getUriBasenameLabel(uri); + addBtn.element.classList.add(...getIconClasses(this.modelService, this.languageService, uri, FileKind.FILE)); + addBtn.setTitle(localize('suggeste.title', "{0} - {1}", this.labelService.getUriLabel(uri, { relative: true }), metadata.description ?? '')); + + this._chatEditsActionsDisposables.add(addBtn.onDidClick(() => { + group.remove(); // REMOVE asap + chatEditingSession.addFileToWorkingSet(uri); + })); + + const rmBtn = this._chatEditsActionsDisposables.add(new Button(addFilesElement, { + supportIcons: false, + secondary: true, + hoverDelegate + })); + rmBtn.icon = Codicon.close; + rmBtn.setTitle(localize('chatEditingSession.removeSuggested', 'Remove suggestion')); + this._chatEditsActionsDisposables.add(rmBtn.onDidClick(() => { + group.remove(); // REMOVE asap + chatEditingSession.remove(WorkingSetEntryRemovalReason.User, uri); + })); + + const sep = document.createElement('div'); + sep.classList.add('separator'); + + const group = document.createElement('span'); + group.classList.add('monaco-button-dropdown', 'sidebyside-button'); + group.appendChild(addBtn.element); + group.appendChild(sep); + group.appendChild(rmBtn.element); + dom.append(addFilesElement, group); + + this._chatEditsActionsDisposables.add(toDisposable(() => { + group.remove(); + })); + } + } async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index c80aaadddfd..097669c3062 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -53,6 +53,7 @@ import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js' import { MarkUnhelpfulActionId } from './actions/chatTitleActions.js'; import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidgetService } from './chat.js'; import { ChatAgentHover, getChatAgentHoverOptions } from './chatAgentHover.js'; +import { ChatAgentCommandContentPart } from './chatContentParts/chatAgentCommandContentPart.js'; import { ChatAttachmentsContentPart } from './chatContentParts/chatAttachmentsContentPart.js'; import { ChatCodeCitationContentPart } from './chatContentParts/chatCodeCitationContentPart.js'; import { ChatCommandButtonContentPart } from './chatContentParts/chatCommandContentPart.js'; @@ -123,8 +124,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer()); readonly onDidClickFollowup: Event = this._onDidClickFollowup.event; - private readonly _onDidClickRerunWithAgentOrCommandDetection = new Emitter(); - readonly onDidClickRerunWithAgentOrCommandDetection: Event = this._onDidClickRerunWithAgentOrCommandDetection.event; + private readonly _onDidClickRerunWithAgentOrCommandDetection = new Emitter<{ sessionId: string; requestId: string }>(); + readonly onDidClickRerunWithAgentOrCommandDetection: Event<{ sessionId: string; requestId: string }> = this._onDidClickRerunWithAgentOrCommandDetection.event; protected readonly _onDidChangeItemHeight = this._register(new Emitter()); readonly onDidChangeItemHeight: Event = this._onDidChangeItemHeight.event; @@ -455,7 +456,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this._onDidClickRerunWithAgentOrCommandDetection.fire({ sessionId: element.sessionId, requestId: element.id })); + templateData.value.appendChild(cmdPart.domNode); + parts.push(cmdPart); + } + templateData.value.appendChild(newPart.domNode); parts.push(newPart); } diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index a7845edbca5..1c183c34200 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -311,8 +311,11 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { }, ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]), when: ContextKeyExpr.or( - ChatContextKeys.Setup.triggered, - ChatContextKeys.Setup.installed, + ContextKeyExpr.and( + ContextKeyExpr.has('config.chat.experimental.offerSetup'), + ContextKeyExpr.or( + ChatContextKeys.Setup.triggered, + ChatContextKeys.Setup.installed)), ChatContextKeys.panelParticipantRegistered, ChatContextKeys.extensionInvalid ) @@ -361,7 +364,9 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { }, ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]), when: ContextKeyExpr.or( - ChatContextKeys.Setup.installed, + ContextKeyExpr.and( + ContextKeyExpr.has('config.chat.experimental.offerSetup'), + ChatContextKeys.Setup.installed), ChatContextKeys.editingParticipantRegistered ) }]; @@ -415,9 +420,9 @@ export class ChatCompatibilityNotifier extends Disposable implements IWorkbenchC this.registeredWelcomeView = true; const showExtensionLabel = localize('showExtension', "Show Extension"); - const mainMessage = localize('chatFailErrorMessage', "Chat failed to load because the installed version of the {0} extension is not compatible with this version of {1}. Please ensure that the {2} extension is up to date.", this.productService.defaultChatAgent?.chatName, this.productService.nameLong, this.productService.defaultChatAgent?.chatName); + const mainMessage = localize('chatFailErrorMessage', "Chat failed to load because the installed version of the Copilot Chat extension is not compatible with this version of {0}. Please ensure that the Copilot Chat extension is up to date.", this.productService.nameLong); const commandButton = `[${showExtensionLabel}](command:${showExtensionsWithIdsCommandId}?${encodeURIComponent(JSON.stringify([[this.productService.defaultChatAgent?.chatExtensionId]]))})`; - const versionMessage = `${this.productService.defaultChatAgent?.chatName} version: ${chatExtension.version}`; + const versionMessage = `Copilot Chat version: ${chatExtension.version}`; const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); this._register(viewsRegistry.registerViewWelcomeContent(ChatViewId, { content: [mainMessage, commandButton, versionMessage].join('\n\n'), diff --git a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts index 62350a6cb09..9d990d09302 100644 --- a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts @@ -7,7 +7,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { createStringDataTransferItem, IDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from '../../../../base/common/dataTransfer.js'; import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; import { IRange } from '../../../../editor/common/core/range.js'; -import { DocumentPasteContext, DocumentPasteEditProvider, DocumentPasteEditsSession } from '../../../../editor/common/languages.js'; +import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession } from '../../../../editor/common/languages.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; @@ -15,13 +15,20 @@ import { ChatInputPart } from './chatInputPart.js'; import { IChatWidgetService } from './chat.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { localize } from '../../../../nls.js'; -import { IChatRequestVariableEntry } from '../common/chatModel.js'; +import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../common/chatModel.js'; import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; import { Mimes } from '../../../../base/common/mime.js'; -import { URI } from '../../../../base/common/uri.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { URI, UriComponents } from '../../../../base/common/uri.js'; +import { basename } from '../../../../base/common/resources.js'; const COPY_MIME_TYPES = 'application/vnd.code.additional-editor-data'; +interface SerializedCopyData { + readonly uri: UriComponents; + readonly range: IRange; +} + export class PasteImageProvider implements DocumentPasteEditProvider { public readonly kind = new HierarchicalKind('chat.attach.image'); @@ -94,7 +101,8 @@ export class PasteImageProvider implements DocumentPasteEditProvider { return; } - return getCustomPaste(model, imageContext, mimeType, this.kind, localize('pastedImageAttachment', 'Pasted Image Attachment'), this.chatWidgetService); + const edit = createCustomPasteEdit(model, imageContext, mimeType, this.kind, localize('pastedImageAttachment', 'Pasted Image Attachment'), this.chatWidgetService); + return createEditSession(edit); } } @@ -149,9 +157,10 @@ export class CopyTextProvider implements DocumentPasteEditProvider { if (model.uri.scheme === ChatInputPart.INPUT_SCHEME) { return; } + const customDataTransfer = new VSDataTransfer(); - const rangesString = JSON.stringify({ ranges: ranges[0], uri: model.uri.toString() }); - customDataTransfer.append(COPY_MIME_TYPES, createStringDataTransferItem(rangesString)); + const data: SerializedCopyData = { range: ranges[0], uri: model.uri.toJSON() }; + customDataTransfer.append(COPY_MIME_TYPES, createStringDataTransferItem(JSON.stringify(data))); return customDataTransfer; } } @@ -165,7 +174,8 @@ export class PasteTextProvider implements DocumentPasteEditProvider { public readonly pasteMimeTypes = [COPY_MIME_TYPES]; constructor( - private readonly chatWidgetService: IChatWidgetService + private readonly chatWidgetService: IChatWidgetService, + private readonly modelService: IModelService ) { } async provideDocumentPasteEdits(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { @@ -182,14 +192,29 @@ export class PasteTextProvider implements DocumentPasteEditProvider { const textdata = await text.asString(); const metadata = JSON.parse(await editorData.asString()); - const additionalData = JSON.parse(await additionalEditorData.asString()); + const additionalData: SerializedCopyData = JSON.parse(await additionalEditorData.asString()); const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); if (!widget) { return; } - const copiedContext = await getCopiedContext(textdata, additionalData.uri, metadata.mode, additionalData.ranges); + const start = additionalData.range.startLineNumber; + const end = additionalData.range.endLineNumber; + if (start === end) { + const textModel = this.modelService.getModel(URI.revive(additionalData.uri)); + if (!textModel) { + return; + } + + // If copied line text data is the entire line content, then we can paste it as a code attachment. Otherwise, we ignore and use default paste provider. + const lineContent = textModel.getLineContent(start); + if (lineContent !== textdata) { + return; + } + } + + const copiedContext = getCopiedContext(textdata, URI.revive(additionalData.uri), metadata.mode, additionalData.range); if (token.isCancellationRequested || !copiedContext) { return; @@ -200,35 +225,41 @@ export class PasteTextProvider implements DocumentPasteEditProvider { return; } - return getCustomPaste(model, copiedContext, Mimes.text, this.kind, localize('pastedCodeAttachment', 'Pasted Code Attachment'), this.chatWidgetService); + const edit = createCustomPasteEdit(model, copiedContext, Mimes.text, this.kind, localize('pastedCodeAttachment', 'Pasted Code Attachment'), this.chatWidgetService); + edit.yieldTo = [{ kind: HierarchicalKind.Empty.append('text', 'plain') }]; + return createEditSession(edit); } } -async function getCopiedContext(code: string, file: string, language: string, ranges: IRange): Promise { - const fileName = file.split('/').pop() || 'unknown file'; - const start = ranges.startLineNumber; - const end = ranges.endLineNumber; +function getCopiedContext(code: string, file: URI, language: string, range: IRange): IChatRequestPasteVariableEntry { + const fileName = basename(file); + const start = range.startLineNumber; + const end = range.endLineNumber; const resultText = `Copied Selection of Code: \n\n\n From the file: ${fileName} From lines ${start} to ${end} \n \`\`\`${code}\`\`\``; const pastedLines = start === end ? localize('pastedAttachment.oneLine', '1 line') : localize('pastedAttachment.multipleLines', '{0} lines', end + 1 - start); return { kind: 'paste', value: resultText, - id: `${fileName}${start}${end}${ranges.startColumn}${ranges.endColumn}`, + id: `${fileName}${start}${end}${range.startColumn}${range.endColumn}`, name: `${fileName} ${pastedLines}`, icon: Codicon.code, isDynamic: true, pastedLines, language, - fileName, + fileName: file.toString(), + copiedFrom: { + uri: file, + range + }, code, references: [{ - reference: URI.parse(file), + reference: file, kind: 'reference' }] }; } -async function getCustomPaste(model: ITextModel, context: IChatRequestVariableEntry, handledMimeType: string, kind: HierarchicalKind, title: string, chatWidgetService: IChatWidgetService): Promise { +function createCustomPasteEdit(model: ITextModel, context: IChatRequestVariableEntry, handledMimeType: string, kind: HierarchicalKind, title: string, chatWidgetService: IChatWidgetService): DocumentPasteEdit { const customEdit = { resource: model.uri, variable: context, @@ -250,13 +281,20 @@ async function getCustomPaste(model: ITextModel, context: IChatRequestVariableEn }; return { - edits: [{ - insertText: '', title, kind, handledMimeType, - additionalEdit: { - edits: [customEdit], - } - }], - dispose() { }, + insertText: '', + title, + kind, + handledMimeType, + additionalEdit: { + edits: [customEdit], + } + }; +} + +function createEditSession(edit: DocumentPasteEdit): DocumentPasteEditsSession { + return { + edits: [edit], + dispose: () => { }, }; } @@ -264,11 +302,12 @@ export class ChatPasteProvidersFeature extends Disposable { constructor( @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService chatWidgetService: IChatWidgetService, - @IExtensionService extensionService: IExtensionService + @IExtensionService extensionService: IExtensionService, + @IModelService modelService: IModelService ) { super(); this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, pattern: '*', hasAccessToAllModels: true }, new PasteImageProvider(chatWidgetService, extensionService))); - this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, pattern: '*', hasAccessToAllModels: true }, new PasteTextProvider(chatWidgetService))); + this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, pattern: '*', hasAccessToAllModels: true }, new PasteTextProvider(chatWidgetService, modelService))); this._register(languageFeaturesService.documentPasteEditProvider.register('*', new CopyTextProvider())); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 35d57c70851..8c2960dfc9a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -3,59 +3,61 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/chatViewSetup.css'; -import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; -import { AuthenticationSession, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; -import { IProductService } from '../../../../platform/product/common/productService.js'; -import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; -import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { IRequestService, asText } from '../../../../platform/request/common/request.js'; +import { $, getActiveElement, setVisibility } from '../../../../base/browser/dom.js'; +import { Button, ButtonWithDropdown } from '../../../../base/browser/ui/button/button.js'; +import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { IAction, toAction } from '../../../../base/common/actions.js'; +import { Barrier, timeout } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IRequestContext } from '../../../../base/parts/request/common/request.js'; +import { Codicon } from '../../../../base/common/codicons.js'; import { isCancellationError } from '../../../../base/common/errors.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Lazy } from '../../../../base/common/lazy.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IRequestContext } from '../../../../base/parts/request/common/request.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; +import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import product from '../../../../platform/product/common/product.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { asText, IRequestService } from '../../../../platform/request/common/request.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; +import { IActivityService, ProgressBadge } from '../../../services/activity/common/activity.js'; +import { AuthenticationSession, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; +import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; +import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; -import { showChatView, ChatViewId, showEditsView, IChatWidget, EditsViewId } from './chat.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import product from '../../../../platform/product/common/product.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { IChatViewsWelcomeContributionRegistry, ChatViewsWelcomeExtensions } from './viewsWelcome/chatViewsWelcome.js'; -import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { $, addDisposableListener, EventType, getActiveElement, setVisibility } from '../../../../base/browser/dom.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; -import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { Button } from '../../../../base/browser/ui/button/button.js'; -import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; -import { Barrier, timeout } from '../../../../base/common/async.js'; import { IChatAgentService } from '../common/chatAgents.js'; -import { IActivityService, ProgressBadge } from '../../../services/activity/common/activity.js'; +import { ChatContextKeys } from '../common/chatContextKeys.js'; +import { CHAT_CATEGORY } from './actions/chatActions.js'; +import { ChatViewId, EditsViewId, ensureSideBarChatViewSize, IChatWidget, showChatView, showEditsView } from './chat.js'; import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import './media/chatViewSetup.css'; +import { ChatViewsWelcomeExtensions, IChatViewsWelcomeContributionRegistry } from './viewsWelcome/chatViewsWelcome.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '', - name: product.defaultChatAgent?.name ?? '', - icon: Codicon[product.defaultChatAgent?.icon as keyof typeof Codicon ?? 'commentDiscussion'], - chatWelcomeTitle: product.defaultChatAgent?.chatWelcomeTitle ?? '', documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? '', skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '', @@ -63,32 +65,32 @@ const defaultChat = { providerName: product.defaultChatAgent?.providerName ?? '', providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '', - entitlementChatEnabled: product.defaultChatAgent?.entitlementChatEnabled ?? '', entitlementSignupLimitedUrl: product.defaultChatAgent?.entitlementSignupLimitedUrl ?? '', - entitlementCanSignupLimited: product.defaultChatAgent?.entitlementCanSignupLimited ?? '', - entitlementSkuType: product.defaultChatAgent?.entitlementSkuType ?? '', - entitlementSkuTypeLimited: product.defaultChatAgent?.entitlementSkuTypeLimited ?? '', - entitlementSkuTypeLimitedName: product.defaultChatAgent?.entitlementSkuTypeLimitedName ?? '' }; enum ChatEntitlement { /** Signed out */ Unknown = 1, - /** Signed in but not yet resolved if Sign-up possible */ + /** Signed in but not yet resolved */ Unresolved, - /** Signed in and entitled to Sign-up */ + /** Signed in and entitled to Limited */ Available, - /** Signed in but not entitled to Sign-up */ - Unavailable + /** Signed in but not entitled to Limited */ + Unavailable, + /** Signed-up to Limited */ + Limited, + /** Signed-up to Pro */ + Pro } //#region Contribution +const TRIGGER_SETUP_COMMAND_ID = 'workbench.action.chat.triggerSetup'; + class ChatSetupContribution extends Disposable implements IWorkbenchContribution { - private readonly chatSetupContext = this._register(this.instantiationService.createInstance(ChatSetupContext)); - private readonly entitlementsResolver = this._register(this.instantiationService.createInstance(ChatSetupEntitlementResolver, this.chatSetupContext)); - private readonly chatSetupController = this._register(this.instantiationService.createInstance(ChatSetupController, this.entitlementsResolver, this.chatSetupContext)); + private readonly context = this._register(this.instantiationService.createInstance(ChatSetupContext)); + private readonly controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, this.context))); constructor( @IProductService private readonly productService: IProductService, @@ -101,78 +103,33 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution } this.registerChatWelcome(); + this.registerActions(); } private registerChatWelcome(): void { Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ - title: defaultChat.chatWelcomeTitle, - when: ContextKeyExpr.or( - ContextKeyExpr.and( - ChatContextKeys.Setup.triggered, - ChatContextKeys.Setup.installed.negate() - )!, - ContextKeyExpr.and( - ChatContextKeys.Setup.canSignUp, - ChatContextKeys.Setup.installed - )! + 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: defaultChat.icon, - content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, this.chatSetupController)).element, + icon: Codicon.copilot, + content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, this.controller.value, this.context)).element, }); } -} - -//#endregion - -//#region Entitlements Resolver - -type ChatSetupEntitlementClassification = { - entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' }; - entitled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is chat setup entitled' }; - limited: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is chat setup limited' }; - owner: 'bpasero'; - comment: 'Reporting chat setup entitlements'; -}; - -type ChatSetupEntitlementEvent = { - entitlement: ChatEntitlement; - entitled: boolean; - limited: boolean; -}; - -interface IResolvedEntitlements { - readonly entitlement: ChatEntitlement; - readonly entitled: boolean; - readonly limited: boolean; -} - -const TRIGGER_SETUP_COMMAND_ID = 'workbench.action.chat.triggerSetup'; - -class ChatSetupEntitlementResolver extends Disposable { - - private _entitlement = ChatEntitlement.Unknown; - get entitlement() { return this._entitlement; } - - private readonly _onDidChangeEntitlement = this._register(new Emitter()); - readonly onDidChangeEntitlement = this._onDidChangeEntitlement.event; - - private pendingResolveCts = new CancellationTokenSource(); - private resolvedEntitlements: IResolvedEntitlements | undefined = undefined; - - constructor( - private readonly chatSetupContext: ChatSetupContext, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @IAuthenticationService private readonly authenticationService: IAuthenticationService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ILogService private readonly logService: ILogService, - ) { - super(); - - this.registerActions(); - this.registerListeners(); - - this.resolve(); - } private registerActions(): void { const that = this; @@ -180,12 +137,13 @@ class ChatSetupEntitlementResolver extends Disposable { class ChatSetupTriggerAction extends Action2 { static readonly ID = TRIGGER_SETUP_COMMAND_ID; - static readonly TITLE = localize2('triggerChatSetup', "Use AI features with {0}...", defaultChat.name); + static readonly TITLE = localize2('triggerChatSetup', "Use AI Features with Copilot for Free..."); constructor() { super({ id: ChatSetupTriggerAction.ID, title: ChatSetupTriggerAction.TITLE, + category: CHAT_CATEGORY, f1: true, precondition: ContextKeyExpr.and( ChatContextKeys.Setup.installed.negate(), @@ -209,18 +167,10 @@ class ChatSetupEntitlementResolver extends Disposable { const configurationService = accessor.get(IConfigurationService); const layoutService = accessor.get(IWorkbenchLayoutService); - await that.chatSetupContext.update({ triggered: true }); + await that.context.update({ triggered: true }); showCopilotView(viewsService); - - const location = viewDescriptorService.getViewLocationById(ChatViewId); - if (location !== ViewContainerLocation.Panel) { - const viewPart = location === ViewContainerLocation.Sidebar ? Parts.SIDEBAR_PART : Parts.AUXILIARYBAR_PART; - const partSize = layoutService.getSize(viewPart); - if (partSize.width < 350) { - layoutService.setSize(viewPart, { width: 350, height: partSize.height }); - } - } + ensureSideBarChatViewSize(400, viewDescriptorService, layoutService); configurationService.updateValue('chat.commandCenter.enabled', true); } @@ -229,13 +179,14 @@ class ChatSetupEntitlementResolver extends Disposable { class ChatSetupHideAction extends Action2 { static readonly ID = 'workbench.action.chat.hideSetup'; - static readonly TITLE = localize2('hideChatSetup', "Hide {0}", defaultChat.name); + static readonly TITLE = localize2('hideChatSetup', "Hide Copilot"); constructor() { super({ id: ChatSetupHideAction.ID, title: ChatSetupHideAction.TITLE, f1: true, + category: CHAT_CATEGORY, precondition: ContextKeyExpr.and( ChatContextKeys.Setup.installed.negate(), ContextKeyExpr.or( @@ -245,8 +196,8 @@ class ChatSetupEntitlementResolver extends Disposable { ), menu: { id: MenuId.ChatCommandCenter, - group: 'z_end', - order: 2, + group: 'z_hide', + order: 1, when: ChatContextKeys.Setup.installed.negate() } }); @@ -259,34 +210,154 @@ class ChatSetupEntitlementResolver extends Disposable { const dialogService = accessor.get(IDialogService); const { confirmed } = await dialogService.confirm({ - message: localize('hideChatSetupConfirm', "Are you sure you want to hide {0}?", defaultChat.name), - detail: localize('hideChatSetupDetail', "You can restore it by running the '{0}' command.", ChatSetupTriggerAction.TITLE.value), - primaryButton: localize('hideChatSetupButton', "Hide {0}", defaultChat.name) + message: localize('hideChatSetupConfirm', "Are you sure you want to hide Copilot?"), + detail: localize('hideChatSetupDetail', "You can restore Copilot by running the '{0}' command.", ChatSetupTriggerAction.TITLE.value), + primaryButton: localize('hideChatSetupButton', "Hide Copilot") }); if (!confirmed) { return; } - const location = viewsDescriptorService.getViewLocationById(ChatViewId); - - await that.chatSetupContext.update({ triggered: false }); - - if (location === ViewContainerLocation.AuxiliaryBar) { - const activeContainers = viewsDescriptorService.getViewContainersByLocation(location).filter(container => viewsDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 0); - if (activeContainers.length === 0) { - layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); // hide if there are no views in the secondary sidebar - } - } + await hideSetupView(viewsDescriptorService, layoutService); configurationService.updateValue('chat.commandCenter.enabled', false); } } - //#endregion + class ChatSetupDisableAction extends Action2 { + + static readonly ID = 'workbench.action.chat.disableCopilot'; + static readonly TITLE = localize2('disableCopilot', "Stop Using Copilot"); + + constructor() { + super({ + id: ChatSetupDisableAction.ID, + title: ChatSetupDisableAction.TITLE, + f1: true, + category: CHAT_CATEGORY, + precondition: ContextKeyExpr.and( + ChatContextKeys.Setup.installed, + ChatContextKeys.Setup.limited + ), + menu: { + id: MenuId.ChatCommandCenter, + group: 'z_hide', + order: 1, + when: ContextKeyExpr.and( + ChatContextKeys.Setup.installed, + ChatContextKeys.Setup.limited + ) + } + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const dialogService = accessor.get(IDialogService); + const extensionManagementService = accessor.get(IExtensionManagementService); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const viewsDescriptorService = accessor.get(IViewDescriptorService); + const layoutService = accessor.get(IWorkbenchLayoutService); + + const { confirmed } = await dialogService.confirm({ + message: localize('disableCopilotConfirm', "Are you sure you want to stop using Copilot?"), + detail: localize('hideChatSetupDetail', "You can restore Copilot by running the '{0}' command.", ChatSetupTriggerAction.TITLE.value), + primaryButton: localize('stopUsingCopilotButton', "Stop Using Copilot") + }); + + if (!confirmed) { + return; + } + + const installed = await extensionManagementService.getInstalled(); + const extensionsToDisable = installed.filter(e => ExtensionIdentifier.equals(e.identifier.id, defaultChat.extensionId)); + await extensionManagementService.uninstall(extensionsToDisable[0]); + + await extensionsWorkbenchService.updateRunningExtensions(); + + await hideSetupView(viewsDescriptorService, layoutService); + } + } + + async function hideSetupView(viewsDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): Promise { + const location = viewsDescriptorService.getViewLocationById(ChatViewId); + + await that.context.update({ triggered: false }); + + if (location === ViewContainerLocation.AuxiliaryBar) { + const activeContainers = viewsDescriptorService.getViewContainersByLocation(location).filter(container => viewsDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 0); + if (activeContainers.length === 0) { + layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); // hide if there are no views in the secondary sidebar + } + } + } registerAction2(ChatSetupTriggerAction); registerAction2(ChatSetupHideAction); + registerAction2(ChatSetupDisableAction); + } +} + +//#endregion + +//#region Chat Setup Request Service + +type EntitlementClassification = { + entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' }; + quotaChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; + quotaCompletions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; + owner: 'bpasero'; + comment: 'Reporting chat setup entitlements'; +}; + +type EntitlementEvent = { + entitlement: ChatEntitlement; + quotaChat: number | undefined; + quotaCompletions: number | undefined; +}; + +interface IEntitlementsResponse { + readonly access_type_sku: string; + readonly assigned_date: string; + readonly can_signup_for_limited: boolean; + readonly chat_enabled: boolean; + readonly limited_user_quotas?: { + readonly chat: number; + readonly completions: number; + }; + readonly limited_user_reset_date: string; +} + +interface IQuotas { + readonly chat?: number; + readonly completions?: number; + readonly resetDate?: string; +} + +interface IChatEntitlements { + readonly entitlement: ChatEntitlement; + readonly quotas?: IQuotas; +} + +class ChatSetupRequests extends Disposable { + + private state: IChatEntitlements = { entitlement: this.context.state.entitlement }; + + private pendingResolveCts = new CancellationTokenSource(); + private didResolveEntitlements = false; + + constructor( + private readonly context: ChatSetupContext, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @ILogService private readonly logService: ILogService, + @IRequestService private readonly requestService: IRequestService, + ) { + super(); + + this.registerListeners(); + + this.resolve(); } private registerListeners(): void { @@ -321,14 +392,21 @@ class ChatSetupEntitlementResolver extends Disposable { } // Immediately signal whether we have a session or not + let state: IChatEntitlements | undefined = undefined; if (session) { - this.update(this.resolvedEntitlements ?? { entitlement: ChatEntitlement.Unresolved, limited: false, entitled: false }); + // Do not overwrite any state we have already + if (this.state.entitlement === ChatEntitlement.Unknown) { + state = { entitlement: ChatEntitlement.Unresolved }; + } } else { - this.resolvedEntitlements = undefined; // reset resolved entitlement when there is no session - this.update({ entitlement: ChatEntitlement.Unknown, limited: false, entitled: false }); + this.didResolveEntitlements = false; // reset so that we resolve entitlements fresh when signed in again + state = { entitlement: ChatEntitlement.Unknown }; + } + if (state) { + this.update(state); } - if (session && typeof this.resolvedEntitlements === 'undefined') { + if (session && !this.didResolveEntitlements) { // Afterwards resolve entitlement with a network request // but only unless it was not already resolved before. await this.resolveEntitlement(session, cts.token); @@ -357,33 +435,33 @@ class ChatSetupEntitlementResolver extends Disposable { } private async resolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { - const resolvedEntitlements = await this.doResolveEntitlement(session, token); - if (typeof resolvedEntitlements?.entitlement === 'number' && !token.isCancellationRequested) { - this.resolvedEntitlements = resolvedEntitlements; - this.update(resolvedEntitlements); + const entitlements = await this.doResolveEntitlement(session, token); + if (typeof entitlements?.entitlement === 'number' && !token.isCancellationRequested) { + this.didResolveEntitlements = true; + this.update(entitlements); } - return resolvedEntitlements?.entitlement; + return entitlements?.entitlement; } - private async doResolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { + private async doResolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { if (token.isCancellationRequested) { return undefined; } - const response = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementUrl, 'GET', undefined, session, token)); + const response = await this.request(defaultChat.entitlementUrl, 'GET', undefined, session, token); if (token.isCancellationRequested) { return undefined; } if (!response) { this.logService.trace('[chat setup] entitlement: no response'); - return { entitlement: ChatEntitlement.Unresolved, limited: false, entitled: false }; + return { entitlement: ChatEntitlement.Unresolved }; } if (response.res.statusCode && response.res.statusCode !== 200) { this.logService.trace(`[chat setup] entitlement: unexpected status code ${response.res.statusCode}`); - return { entitlement: ChatEntitlement.Unresolved, limited: false, entitled: false }; + return { entitlement: ChatEntitlement.Unresolved }; } const responseText = await asText(response); @@ -393,45 +471,122 @@ class ChatSetupEntitlementResolver extends Disposable { if (!responseText) { this.logService.trace('[chat setup] entitlement: response has no content'); - return { entitlement: ChatEntitlement.Unresolved, limited: false, entitled: false }; + return { entitlement: ChatEntitlement.Unresolved }; } - let parsedResult: any; + let entitlementsResponse: IEntitlementsResponse; try { - parsedResult = JSON.parse(responseText); - this.logService.trace(`[chat setup] entitlement: parsed result is ${JSON.stringify(parsedResult)}`); + entitlementsResponse = JSON.parse(responseText); + this.logService.trace(`[chat setup] entitlement: parsed result is ${JSON.stringify(entitlementsResponse)}`); } catch (err) { this.logService.trace(`[chat setup] entitlement: error parsing response (${err})`); - return { entitlement: ChatEntitlement.Unresolved, limited: false, entitled: false }; + return { entitlement: ChatEntitlement.Unresolved }; } - const result = { - entitlement: Boolean(parsedResult[defaultChat.entitlementCanSignupLimited]) ? ChatEntitlement.Available : ChatEntitlement.Unavailable, - entitled: Boolean(parsedResult[defaultChat.entitlementChatEnabled]), - limited: Boolean(parsedResult[defaultChat.entitlementSkuType] === defaultChat.entitlementSkuTypeLimited) + let entitlement: ChatEntitlement; + if (entitlementsResponse.access_type_sku === 'free_limited_copilot') { + entitlement = ChatEntitlement.Limited; + } else if (entitlementsResponse.can_signup_for_limited) { + entitlement = ChatEntitlement.Available; + } else if (entitlementsResponse.chat_enabled) { + entitlement = ChatEntitlement.Pro; + } else { + entitlement = ChatEntitlement.Unavailable; + } + + const entitlements: IChatEntitlements = { + entitlement, + quotas: { + chat: entitlementsResponse.limited_user_quotas?.chat, + completions: entitlementsResponse.limited_user_quotas?.completions, + resetDate: entitlementsResponse.limited_user_reset_date + } }; - this.logService.trace(`[chat setup] entitlement: resolved to ${result.entitlement}, entitled: ${result.entitled}, limited: ${result.limited}`); - this.telemetryService.publicLog2('chatInstallEntitlement', result); + this.logService.trace(`[chat setup] entitlement: resolved to ${entitlements.entitlement}, quotas: ${JSON.stringify(entitlements.quotas)}`); + this.telemetryService.publicLog2('chatInstallEntitlement', { + entitlement: entitlements.entitlement, + quotaChat: entitlementsResponse.limited_user_quotas?.chat, + quotaCompletions: entitlementsResponse.limited_user_quotas?.completions + }); - return result; + return entitlements; } - private update({ entitlement: newEntitlement, limited, entitled }: IResolvedEntitlements): void { - const oldEntitlement = this._entitlement; - this._entitlement = newEntitlement; + private async request(url: string, type: 'GET', body: undefined, session: AuthenticationSession, token: CancellationToken): Promise; + private async request(url: string, type: 'POST', body: object, session: AuthenticationSession, token: CancellationToken): Promise; + private async request(url: string, type: 'GET' | 'POST', body: object | undefined, session: AuthenticationSession, token: CancellationToken): Promise { + try { + return await this.requestService.request({ + type, + url, + data: type === 'POST' ? JSON.stringify(body) : undefined, + headers: { + 'Authorization': `Bearer ${session.accessToken}` + } + }, token); + } catch (error) { + this.logService.error(`[chat setup] request: error ${error}`); - this.chatSetupContext.update({ canSignUp: newEntitlement === ChatEntitlement.Available, limited, entitled }); - - if (oldEntitlement !== this._entitlement) { - this._onDidChangeEntitlement.fire(this._entitlement); + return undefined; } } + private update(state: IChatEntitlements): void { + this.state = state; + + this.context.update({ entitlement: this.state.entitlement }); + } + async forceResolveEntitlement(session: AuthenticationSession): Promise { return this.resolveEntitlement(session, CancellationToken.None); } + async signUpLimited(session: AuthenticationSession): Promise { + const body = { + restricted_telemetry: 'disabled', + public_code_suggestions: 'enabled' + }; + + const response = await this.request(defaultChat.entitlementSignupLimitedUrl, 'POST', body, session, CancellationToken.None); + if (!response) { + this.logService.error('[chat setup] sign-up: no response'); + return false; + } + + if (response.res.statusCode && response.res.statusCode !== 200) { + this.logService.error(`[chat setup] sign-up: unexpected status code ${response.res.statusCode}`); + return false; + } + + const responseText = await asText(response); + if (!responseText) { + this.logService.error('[chat setup] sign-up: response has no content'); + return false; + } + + let parsedResult: { subscribed: boolean } | undefined = undefined; + try { + parsedResult = JSON.parse(responseText); + this.logService.trace(`[chat setup] sign-up: response is ${responseText}`); + } catch (err) { + this.logService.error(`[chat setup] sign-up: error parsing response (${err})`); + } + + const subscribed = Boolean(parsedResult?.subscribed); + if (subscribed) { + this.logService.trace('[chat setup] sign-up: successfully subscribed'); + } else { + this.logService.error('[chat setup] sign-up: not subscribed'); + } + + if (subscribed) { + this.update({ entitlement: ChatEntitlement.Limited }); + } + + return subscribed; + } + override dispose(): void { this.pendingResolveCts.dispose(true); @@ -470,21 +625,12 @@ class ChatSetupController extends Disposable { return this._step; } - get entitlement(): ChatEntitlement { - return this.entitlementResolver.entitlement; - } - - get canSignUpLimited(): boolean { - return this.entitlement === ChatEntitlement.Available || // user can sign up for limited - this.entitlement === ChatEntitlement.Unresolved; // user unresolved, play safe and allow - } + private readonly requests = this._register(this.instantiationService.createInstance(ChatSetupRequests, this.context)); constructor( - private readonly entitlementResolver: ChatSetupEntitlementResolver, - private readonly chatSetupContext: ChatSetupContext, + private readonly context: ChatSetupContext, @ITelemetryService private readonly telemetryService: ITelemetryService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IViewsService private readonly viewsService: IViewsService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IProductService private readonly productService: IProductService, @@ -492,7 +638,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, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); @@ -500,10 +647,10 @@ class ChatSetupController extends Disposable { } private registerListeners(): void { - this._register(this.entitlementResolver.onDidChangeEntitlement(() => this._onDidChange.fire())); + this._register(this.context.onDidChange(() => this._onDidChange.fire())); } - setStep(step: ChatSetupStep): void { + private setStep(step: ChatSetupStep): void { if (this._step === step) { return; } @@ -512,8 +659,8 @@ class ChatSetupController extends Disposable { this._onDidChange.fire(); } - async setup(enableTelemetry: boolean): Promise { - const title = localize('setupChatProgress', "Getting {0} ready...", defaultChat.name); + async setup(): Promise { + const title = localize('setupChatProgress', "Getting Copilot ready..."); const badge = this.activityService.showViewContainerActivity(isCopilotEditsViewActive(this.viewsService) ? CHAT_EDITING_SIDEBAR_PANEL_ID : CHAT_SIDEBAR_PANEL_ID, { badge: new ProgressBadge(() => title), priority: 100 @@ -524,49 +671,53 @@ class ChatSetupController extends Disposable { location: ProgressLocation.Window, command: TRIGGER_SETUP_COMMAND_ID, title, - }, () => this.doSetup(enableTelemetry)); + }, () => this.doSetup()); } finally { badge.dispose(); } } - private async doSetup(enableTelemetry: boolean): Promise { + private async doSetup(): Promise { + this.context.suspend(); // reduces flicker try { let session: AuthenticationSession | undefined; + let entitlement: ChatEntitlement | undefined; // Entitlement Unknown: we need to sign-in user - if (this.entitlement === ChatEntitlement.Unknown) { + if (this.context.state.entitlement === ChatEntitlement.Unknown) { this.setStep(ChatSetupStep.SigningIn); - session = await this.signIn(); - if (!session) { + const result = await this.signIn(); + if (!result.session) { return; // user cancelled } - const entitlement = await this.entitlementResolver.forceResolveEntitlement(session); - if (entitlement !== ChatEntitlement.Unavailable) { - return; // we cannot proceed with automated install because user needs to sign-up in a second step - } + session = result.session; + entitlement = result.entitlement; } - // Entitlement known: proceed with installation if (!session) { session = (await this.authenticationService.getSessions(defaultChat.providerId)).at(0); if (!session) { return; // unexpected } } + + // Install this.setStep(ChatSetupStep.Installing); - await this.install(session, enableTelemetry); + await this.install(session, entitlement ?? this.context.state.entitlement); } finally { this.setStep(ChatSetupStep.Initial); + this.context.resume(); } } - private async signIn(): Promise { + private async signIn(): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> { let session: AuthenticationSession | undefined; + let entitlement: ChatEntitlement | undefined; try { showCopilotView(this.viewsService); session = await this.authenticationService.createSession(defaultChat.providerId, defaultChat.providerScopes[0]); + entitlement = await this.requests.forceResolveEntitlement(session); } catch (error) { // noop } @@ -575,25 +726,21 @@ class ChatSetupController extends Disposable { this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false }); } - return session; + return { session, entitlement }; } - private async install(session: AuthenticationSession, enableTelemetry: boolean): Promise { + private async install(session: AuthenticationSession, entitlement: ChatEntitlement): Promise { const signedIn = !!session; const activeElement = getActiveElement(); - let installResult: 'installed' | 'cancelled' | 'failedInstall'; - const wasInstalled = this.chatSetupContext.getContext().installed; + let installResult: 'installed' | 'cancelled' | 'failedInstall' | undefined = undefined; + const wasInstalled = this.context.state.installed; let didSignUp = false; try { showCopilotView(this.viewsService); - this.chatSetupContext.suspend(); // reduces flicker - - if (this.canSignUpLimited) { - didSignUp = await this.signUpLimited(session, enableTelemetry); - } else { - this.logService.trace('[chat setup] install: not signing up to limited SKU'); + if (entitlement !== ChatEntitlement.Limited && entitlement !== ChatEntitlement.Pro && entitlement !== ChatEntitlement.Unavailable) { + didSignUp = await this.requests.signUpLimited(session); } await this.extensionsWorkbenchService.install(defaultChat.extensionId, { @@ -613,10 +760,12 @@ class ChatSetupController extends Disposable { this.commandService.executeCommand('github.copilot.refreshToken'); // ugly, but we need to signal to the extension that sign-up happened } - await Promise.race([ - timeout(5000), // helps prevent flicker with sign-in welcome view - Event.toPromise(this.chatAgentService.onDidChangeAgents) // https://github.com/microsoft/vscode-copilot/issues/9274 - ]).finally(() => this.chatSetupContext.resume()); + if (installResult === 'installed') { + await Promise.race([ + timeout(5000), // helps prevent flicker with sign-in welcome view + Event.toPromise(this.chatAgentService.onDidChangeAgents) // https://github.com/microsoft/vscode-copilot/issues/9274 + ]); + } } this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn }); @@ -625,49 +774,6 @@ class ChatSetupController extends Disposable { (await showCopilotView(this.viewsService))?.focusInput(); } } - - private async signUpLimited(session: AuthenticationSession, enableTelemetry: boolean): Promise { - const body = { - restricted_telemetry: enableTelemetry ? 'enabled' : 'disabled' - }; - this.logService.trace(`[chat setup] sign-up: options ${JSON.stringify(body)}`); - - const response = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSignupLimitedUrl, 'POST', body, session, CancellationToken.None)); - if (!response) { - this.logService.error('[chat setup] sign-up: no response'); - return false; - } - - if (response.res.statusCode && response.res.statusCode !== 200) { - this.logService.error(`[chat setup] sign-up: unexpected status code ${response.res.statusCode}`); - return false; - } - - const responseText = await asText(response); - if (!responseText) { - this.logService.error('[chat setup] sign-up: response has no content'); - return false; - } - - let parsedResult: { subscribed: boolean } | undefined = undefined; - try { - parsedResult = JSON.parse(responseText); - this.logService.trace(`[chat setup] sign-up: response is ${responseText}`); - } catch (err) { - this.logService.error(`[chat setup] sign-up: error parsing response (${err})`); - } - - const subscribed = Boolean(parsedResult?.subscribed); - if (subscribed) { - this.logService.trace('[chat setup] sign-up: successfully subscribed'); - } else { - this.logService.error('[chat setup] sign-up: not subscribed'); - } - - this.chatSetupContext.update({ entitled: false, limited: subscribed, canSignUp: !subscribed }); - - return subscribed; - } } class ChatSetupWelcomeContent extends Disposable { @@ -676,8 +782,10 @@ class ChatSetupWelcomeContent extends Disposable { constructor( private readonly controller: ChatSetupController, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @IInstantiationService private readonly instantiationService: IInstantiationService + private readonly context: ChatSetupContext, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @ICommandService private readonly commandService: ICommandService ) { super(); @@ -688,80 +796,108 @@ class ChatSetupWelcomeContent extends Disposable { const markdown = this._register(this.instantiationService.createInstance(MarkdownRenderer, {})); // Header - const header = localize({ key: 'setupHeader', comment: ['{Locked="[{0}]({1})"}'] }, "[{0}]({1}) is your AI pair programmer that helps you with code suggestions, answers your questions, and more.", defaultChat.name, defaultChat.documentationUrl); - this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(header, { isTrusted: true }))).element); + { + const header = localize({ key: 'setupHeader', comment: ['{Locked="[Copilot]({0})"}'] }, "[Copilot]({0} 'Copilot') is your AI pair programmer.", this.context.state.installed ? 'command:github.copilot.open.walkthrough' : defaultChat.documentationUrl); + this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(header, { isTrusted: true }))).element); - const limitedSkuHeader = localize({ key: 'limitedSkuHeader', comment: ['{Locked="[{0}]({1})"}'] }, "Enable powerful AI features for free with the [{0}]({1}) plan.", defaultChat.entitlementSkuTypeLimitedName, defaultChat.skusDocumentationUrl); - const limitedSkuHeaderElement = this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(limitedSkuHeader, { isTrusted: true }))).element); + const features = this.element.appendChild($('div.chat-features-container')); + this.element.appendChild(features); - // Limited SKU Sign-up - const telemetryLabel = localize('telemetryLabel', "Allow {0} to use my data, including prompts, suggestions, and code snippets, for product improvements", defaultChat.providerName); - const { container: telemetryContainer, checkbox: telemetryCheckbox } = this.createCheckBox(telemetryLabel, this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? false : true, markdown); + const featureChatContainer = features.appendChild($('div.chat-feature-container')); + featureChatContainer.appendChild(renderIcon(Codicon.code)); + + const featureChatLabel = featureChatContainer.appendChild($('span')); + featureChatLabel.textContent = localize('featureChat', "Code faster with completions and Inline Chat"); + + const featureEditsContainer = features.appendChild($('div.chat-feature-container')); + featureEditsContainer.appendChild(renderIcon(Codicon.editSession)); + + const featureEditsLabel = featureEditsContainer.appendChild($('span')); + featureEditsLabel.textContent = localize('featureEdits', "Build features and resolve bugs with Copilot Edits"); + + const featureExploreContainer = features.appendChild($('div.chat-feature-container')); + featureExploreContainer.appendChild(renderIcon(Codicon.commentDiscussion)); + + const featureExploreLabel = featureExploreContainer.appendChild($('span')); + featureExploreLabel.textContent = localize('featureExplore', "Explore your codebase with chat"); + } + + // Limited SKU + const limitedSkuHeader = localize({ key: 'limitedSkuHeader', comment: ['{Locked="[]({1})"}'] }, "$(sparkle-filled) We now offer [Copilot for free]({0}).", defaultChat.skusDocumentationUrl); + const limitedSkuHeaderContainer = this.element.appendChild($('p')); + limitedSkuHeaderContainer.appendChild(this._register(markdown.render(new MarkdownString(limitedSkuHeader, { isTrusted: true, supportThemeIcons: true }))).element); + + // Terms + const terms = localize({ key: 'termsLabel', comment: ['{Locked="["}', '{Locked="]({0})"}'] }, "By continuing, you agree to our [Terms and Conditions]({0}).", defaultChat.privacyStatementUrl); + this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(terms, { isTrusted: true }))).element); // Setup Button - const buttonRow = this.element.appendChild($('p')); - const button = this._register(new Button(buttonRow, { ...defaultButtonStyles, supportIcons: true })); - this._register(button.onDidClick(() => this.controller.setup(telemetryCheckbox.checked))); - - // Footer - const footer = localize({ key: 'privacyFooter', comment: ['{Locked="["}', '{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}).", defaultChat.privacyStatementUrl); - this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(footer, { isTrusted: true }))).element); + const actions: IAction[] = []; + if (this.context.state.installed) { + actions.push(toAction({ id: 'chatSetup.signInGh', label: localize('signInGh', "Sign in with a GitHub.com Account"), run: () => this.commandService.executeCommand('github.copilotChat.signIn') })); + actions.push(toAction({ id: 'chatSetup.signInGhe', label: localize('signInGhe', "Sign in with a GHE.com Account"), run: () => this.commandService.executeCommand('github.copilotChat.signInGHE') })); + } + const buttonContainer = this.element.appendChild($('p')); + const button = this._register(actions.length === 0 ? new Button(buttonContainer, { + supportIcons: true, + ...defaultButtonStyles + }) : new ButtonWithDropdown(buttonContainer, { + actions, + addPrimaryActionToDropdown: false, + contextMenuProvider: this.contextMenuService, + supportIcons: true, + ...defaultButtonStyles + })); + this._register(button.onDidClick(() => this.controller.setup())); // Update based on model state - this._register(Event.runAndSubscribe(this.controller.onDidChange, () => this.update(limitedSkuHeaderElement, [telemetryContainer], [telemetryCheckbox], button))); + this._register(Event.runAndSubscribe(this.controller.onDidChange, () => this.update(limitedSkuHeaderContainer, button))); } - private createCheckBox(label: string, checked: boolean, markdown: MarkdownRenderer): { container: HTMLElement; checkbox: Checkbox } { - const container = this.element.appendChild($('p.checkbox-container')); - const checkbox = this._register(new Checkbox(label, checked, defaultCheckboxStyles)); - container.appendChild(checkbox.domNode); + private update(limitedSkuHeaderContainer: HTMLElement, button: Button | ButtonWithDropdown): void { + let showLimitedSkuHeader: boolean; + let buttonLabel: string; - const checkboxLabel = container.appendChild(this._register(markdown.render(new MarkdownString(label, { isTrusted: true, supportThemeIcons: true }), { inline: true, className: 'checkbox-label' })).element); - this._register(addDisposableListener(checkboxLabel, EventType.CLICK, e => { - if (checkbox?.enabled && (e.target as HTMLElement).tagName !== 'A') { - checkbox.checked = !checkbox.checked; - checkbox.focus(); - } - })); - - return { container, checkbox }; - } - - private update(limitedSkuHeaderElement: HTMLElement, limitedCheckboxContainers: HTMLElement[], limitedCheckboxes: Checkbox[], button: Button): void { - switch (this.controller.step) { - case ChatSetupStep.Initial: - setVisibility(this.controller.canSignUpLimited || this.controller.entitlement === ChatEntitlement.Unknown, limitedSkuHeaderElement); - setVisibility(this.controller.canSignUpLimited, ...limitedCheckboxContainers); - - for (const checkbox of limitedCheckboxes) { - checkbox.enable(); - } - - button.enabled = true; - button.label = this.controller.canSignUpLimited ? - localize('startSetupLimited', "Start Using {0}", defaultChat.entitlementSkuTypeLimitedName) : this.controller.entitlement === ChatEntitlement.Unknown ? - localize('signInToStartSetup', "Sign in to Start") : - localize('startSetupLimited', "Start Using {0}", defaultChat.name); + switch (this.context.state.entitlement) { + case ChatEntitlement.Unknown: + case ChatEntitlement.Unresolved: + case ChatEntitlement.Available: + showLimitedSkuHeader = true; + buttonLabel = localize('signInUp', "Sign in to Use Copilot for Free"); break; - case ChatSetupStep.SigningIn: - case ChatSetupStep.Installing: - for (const checkbox of limitedCheckboxes) { - checkbox.disable(); - } - - button.enabled = false; - button.label = this.controller.step === ChatSetupStep.SigningIn ? - localize('setupChatSigningIn', "$(loading~spin) Signing in to {0}...", defaultChat.providerName) : - localize('setupChatInstalling', "$(loading~spin) Getting {0} ready...", defaultChat.name); - + case ChatEntitlement.Limited: + showLimitedSkuHeader = true; + buttonLabel = localize('startUpLimited', "Use Copilot for Free"); + break; + case ChatEntitlement.Pro: + case ChatEntitlement.Unavailable: + showLimitedSkuHeader = false; + buttonLabel = localize('startUp', "Use Copilot"); break; } + + switch (this.controller.step) { + case ChatSetupStep.Initial: + // do not override + break; + case ChatSetupStep.SigningIn: + buttonLabel = localize('setupChatSignIn', "$(loading~spin) Signing in to {0}...", defaultChat.providerName); + break; + case ChatSetupStep.Installing: + buttonLabel = localize('setupChatInstalling', "$(loading~spin) Getting Copilot Ready..."); + break; + } + + setVisibility(showLimitedSkuHeader, limitedSkuHeaderContainer); + + button.label = buttonLabel; + button.enabled = this.controller.step === ChatSetupStep.Initial; } } //#endregion -//#region Helpers +//#region Context function isCopilotEditsViewActive(viewsService: IViewsService): boolean { return viewsService.getFocusedView()?.id === EditsViewId; @@ -775,56 +911,41 @@ function showCopilotView(viewsService: IViewsService): Promise; - static async request(accessor: ServicesAccessor, url: string, type: 'POST', body: object, session: AuthenticationSession, token: CancellationToken): Promise; - static async request(accessor: ServicesAccessor, url: string, type: 'GET' | 'POST', body: object | undefined, session: AuthenticationSession, token: CancellationToken): Promise { - const requestService = accessor.get(IRequestService); - const logService = accessor.get(ILogService); - - try { - return await requestService.request({ - type, - url, - data: type === 'POST' ? JSON.stringify(body) : undefined, - headers: { - 'Authorization': `Bearer ${session.accessToken}` - } - }, token); - } catch (error) { - logService.error(`[chat setup] request: error ${error}`); - - return undefined; - } - } +interface IChatSetupContextState { + entitlement: ChatEntitlement; + triggered?: boolean; + installed?: boolean; } class ChatSetupContext extends Disposable { - private static readonly CHAT_SETUP_TRIGGERD = 'chat.setupTriggered'; - private static readonly CHAT_EXTENSION_INSTALLED = 'chat.extensionInstalled'; + private static readonly CHAT_SETUP_CONTEXT_STORAGE_KEY = 'chat.setupContext'; + private readonly canSignUpContextKey = ChatContextKeys.Setup.canSignUp.bindTo(this.contextKeyService); + private readonly signedOutContextKey = ChatContextKeys.Setup.signedOut.bindTo(this.contextKeyService); private readonly entitledContextKey = ChatContextKeys.Setup.entitled.bindTo(this.contextKeyService); private readonly limitedContextKey = ChatContextKeys.Setup.limited.bindTo(this.contextKeyService); - private readonly canSignUpContextKey = ChatContextKeys.Setup.canSignUp.bindTo(this.contextKeyService); private readonly triggeredContext = ChatContextKeys.Setup.triggered.bindTo(this.contextKeyService); private readonly installedContext = ChatContextKeys.Setup.installed.bindTo(this.contextKeyService); - private triggered = this.storageService.getBoolean(ChatSetupContext.CHAT_SETUP_TRIGGERD, StorageScope.PROFILE, false); - private installed = this.storageService.getBoolean(ChatSetupContext.CHAT_EXTENSION_INSTALLED, StorageScope.PROFILE, false); - private limited = false; - private entitled = false; - private canSignUp = false; + private _state: IChatSetupContextState = this.storageService.getObject(ChatSetupContext.CHAT_SETUP_CONTEXT_STORAGE_KEY, StorageScope.PROFILE) ?? { entitlement: ChatEntitlement.Unknown }; + private suspendedState: IChatSetupContextState | undefined = undefined; + get state(): IChatSetupContextState { + return this.suspendedState ?? this._state; + } - private contextKeyUpdateBarrier: Barrier | undefined = undefined; + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + private updateBarrier: Barrier | undefined = undefined; constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IExtensionService private readonly extensionService: IExtensionService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService ) { super(); @@ -836,96 +957,87 @@ class ChatSetupContext extends Disposable { this._register(this.extensionService.onDidChangeExtensions(result => { for (const extension of result.removed) { if (ExtensionIdentifier.equals(defaultChat.extensionId, extension.identifier)) { - this.update({ chatInstalled: false }); + this.update({ installed: false }); break; } } for (const extension of result.added) { if (ExtensionIdentifier.equals(defaultChat.extensionId, extension.identifier)) { - this.update({ chatInstalled: true }); + this.update({ installed: true }); break; } } })); const extensions = await this.extensionManagementService.getInstalled(); - - const chatInstalled = !!extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); - this.update({ chatInstalled }); + const defaultChatExtension = extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); + this.update({ installed: !!defaultChatExtension && this.extensionEnablementService.isEnabled(defaultChatExtension) }); } - update(context: { chatInstalled: boolean }): Promise; + update(context: { installed: boolean }): Promise; update(context: { triggered: boolean }): Promise; - update(context: { limited: boolean; entitled: boolean; canSignUp: boolean }): Promise; - update(context: { triggered?: boolean; chatInstalled?: boolean; limited?: boolean; entitled?: boolean; canSignUp?: boolean }): Promise { - if (typeof context.chatInstalled === 'boolean') { - this.installed = context.chatInstalled; - this.storageService.store(ChatSetupContext.CHAT_EXTENSION_INSTALLED, context.chatInstalled, StorageScope.PROFILE, StorageTarget.MACHINE); + update(context: { entitlement: ChatEntitlement }): Promise; + update(context: { installed?: boolean; triggered?: boolean; entitlement?: ChatEntitlement }): Promise { + if (typeof context.installed === 'boolean') { + this._state.installed = context.installed; - if (context.chatInstalled) { + if (context.installed) { context.triggered = true; // allows to fallback to setup view if the extension is uninstalled } } if (typeof context.triggered === 'boolean') { - this.triggered = context.triggered; - if (context.triggered) { - this.storageService.store(ChatSetupContext.CHAT_SETUP_TRIGGERD, true, StorageScope.PROFILE, StorageTarget.MACHINE); - } else { - this.storageService.remove(ChatSetupContext.CHAT_SETUP_TRIGGERD, StorageScope.PROFILE); - } + this._state.triggered = context.triggered; } - if (typeof context.limited === 'boolean') { - this.limited = context.limited; + if (typeof context.entitlement === 'number') { + this._state.entitlement = context.entitlement; } - if (typeof context.entitled === 'boolean') { - this.entitled = context.entitled; - } - - if (typeof context.canSignUp === 'boolean') { - this.canSignUp = context.canSignUp; - } + this.storageService.store(ChatSetupContext.CHAT_SETUP_CONTEXT_STORAGE_KEY, this._state, StorageScope.PROFILE, StorageTarget.MACHINE); return this.updateContext(); } private async updateContext(): Promise { - await this.contextKeyUpdateBarrier?.wait(); + await this.updateBarrier?.wait(); - const showChatSetup = this.triggered && !this.installed; - if (showChatSetup) { + if (this._state.triggered && !this._state.installed) { // this is ugly but fixes flicker from a previous chat install this.storageService.remove('chat.welcomeMessageContent.panel', StorageScope.APPLICATION); this.storageService.remove('interactive.sessions', this.workspaceContextService.getWorkspace().folders.length ? StorageScope.WORKSPACE : StorageScope.APPLICATION); } - this.triggeredContext.set(showChatSetup); - this.installedContext.set(this.installed); - this.limitedContextKey.set(this.limited); - this.entitledContextKey.set(this.entitled); - this.canSignUpContextKey.set(this.canSignUp); + let changed = false; + changed = this.updateContextKey(this.signedOutContextKey, this._state.entitlement === ChatEntitlement.Unknown) || changed; + changed = this.updateContextKey(this.canSignUpContextKey, this._state.entitlement === ChatEntitlement.Available) || changed; + changed = this.updateContextKey(this.limitedContextKey, this._state.entitlement === ChatEntitlement.Limited) || changed; + changed = this.updateContextKey(this.entitledContextKey, this._state.entitlement === ChatEntitlement.Pro) || changed; + changed = this.updateContextKey(this.triggeredContext, !!this._state.triggered) || changed; + changed = this.updateContextKey(this.installedContext, !!this._state.installed) || changed; + + if (changed) { + this._onDidChange.fire(); + } } - getContext() { - return { - triggered: this.triggered, - installed: this.installed, - limited: this.limited, - entitled: this.entitled, - canSignUp: this.canSignUp - }; + private updateContextKey(contextKey: IContextKey, value: boolean): boolean { + const current = contextKey.get(); + contextKey.set(value); + + return current !== value; } suspend(): void { - this.contextKeyUpdateBarrier = new Barrier(); + this.suspendedState = { ...this._state }; + this.updateBarrier = new Barrier(); } resume(): void { - this.contextKeyUpdateBarrier?.open(); - this.contextKeyUpdateBarrier = undefined; + this.suspendedState = undefined; + this.updateBarrier?.open(); + this.updateBarrier = undefined; } } diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index e4c4c1cf515..b671262cd99 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { $ } 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'; @@ -154,6 +155,7 @@ 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'); this._widget = this._register(scopedInstantiationService.createInstance( ChatWidget, this.chatOptions.location, @@ -169,7 +171,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { return this.chatOptions.location === ChatAgentLocation.EditingSession; }, }, - enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel + enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel, + editorOverflowWidgetsDomNode: editorOverflowNode, }, { listForeground: SIDE_BAR_FOREGROUND, @@ -184,6 +187,7 @@ 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 e7792dc1d9a..87a35990ac7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -505,7 +505,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } private onDidChangeItems(skipDynamicLayout?: boolean) { - if (this.tree && this._visible) { + if (this._visible || !this.viewModel) { const treeItems = (this.viewModel?.getItems() ?? []) .map((item): ITreeElement => { return { @@ -519,6 +519,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this._onWillMaybeChangeHeight.fire(); + this.lastItem = treeItems.at(-1)?.element; + ChatContextKeys.lastItemId.bindTo(this.contextKeyService).set(this.lastItem ? [this.lastItem.id] : []); this.tree.setChildren(null, treeItems, { diffIdentityProvider: { getId: (element) => { @@ -543,10 +545,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.layoutDynamicChatTreeItemMode(); } - this.lastItem = treeItems[treeItems.length - 1]?.element; - if (this.lastItem) { - ChatContextKeys.lastItemId.bindTo(this.contextKeyService).set([this.lastItem.id]); - } if (this.lastItem && isResponseVM(this.lastItem) && this.lastItem.isComplete) { this.renderFollowups(this.lastItem.replyFollowups, this.lastItem); } else if (!treeItems.length && this.viewModel) { diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 1e6a5a9ea56..1c05029923c 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -8,7 +8,7 @@ import './codeBlockPart.css'; import * as dom from '../../../../base/browser/dom.js'; import { renderFormattedText } from '../../../../base/browser/formattedTextRenderer.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { combinedDisposable, Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; @@ -69,6 +69,8 @@ import { ChatTreeItem } from './chat.js'; import { IChatRendererDelegate } from './chatListRenderer.js'; import { ChatEditorOptions } from './chatOptions.js'; import { emptyProgressRunner, IEditorProgressService } from '../../../../platform/progress/common/progress.js'; +import { AsyncIterableObject } from '../../../../base/common/async.js'; +import { InlineChatController } from '../../inlineChat/browser/inlineChatController.js'; const $ = dom.$; @@ -764,11 +766,11 @@ export class CodeCompareBlockPart extends Disposable { let template: string; if (data.edit.state.applied === 1) { - template = localize('chat.edits.1', "Made 1 change in [[``{0}``]]", uriLabel); + template = localize('chat.edits.1', "Applied 1 change in [[``{0}``]]", uriLabel); } else if (data.edit.state.applied < 0) { template = localize('chat.edits.rejected', "Edits in [[``{0}``]] have been rejected", uriLabel); } else { - template = localize('chat.edits.N', "Made {0} changes in [[``{1}``]]", data.edit.state.applied, uriLabel); + template = localize('chat.edits.N', "Applied {0} changes in [[``{1}``]]", data.edit.state.applied, uriLabel); } const message = renderFormattedText(template, { @@ -938,4 +940,38 @@ export class DefaultChatTextEditor { response.setEditApplied(item, -1); } + + async preview(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup) { + if (item.state?.applied) { + // already applied + return false; + } + + if (!response.response.value.includes(item)) { + // bogous item + return false; + } + + const firstEdit = item.edits[0]?.[0]; + if (!firstEdit) { + return false; + } + const textEdits = AsyncIterableObject.fromArray(item.edits); + + const editorToApply = await this.editorService.openCodeEditor({ resource: item.uri }, null); + if (editorToApply) { + const inlineChatController = InlineChatController.get(editorToApply); + if (inlineChatController) { + const tokenSource = new CancellationTokenSource(); + editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber); + const promise = inlineChatController.reviewEdits(firstEdit.range, textEdits, tokenSource.token); + response.setEditApplied(item, 1); + promise.finally(() => { + tokenSource.dispose(); + }); + return true; + } + } + return false; + } } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 731444a75a2..9d1a1e19450 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -91,7 +91,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC s = []; } - this._variables = s; + this._variables = s.filter(isDynamicVariable); this.updateDecorations(); } @@ -122,6 +122,16 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC } } +/** + * Loose check to filter objects that are obviously missing data + */ +function isDynamicVariable(obj: any): obj is IDynamicVariable { + return obj && + typeof obj.id === 'string' && + Range.isIRange(obj.range) && + 'data' in obj; +} + ChatWidget.CONTRIBS.push(ChatDynamicVariableModel); interface SelectAndInsertActionContext { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts index 5d4768232e2..ff1938c5b7b 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts @@ -6,7 +6,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Event } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { ResourceSet } from '../../../../../base/common/map.js'; +import { ResourceMap } from '../../../../../base/common/map.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; @@ -38,59 +38,61 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben } const currentEditingSession = this.chatEditingService.currentEditingSessionObs.get(); - if (currentEditingSession) { - const workingSetEntries = currentEditingSession.entries.get(); - if (workingSetEntries.length > 0) { - // Do this only for the initial working set state - return; - } - - const widget = this.chatWidgetService.getWidgetBySessionId(currentEditingSession.chatSessionId); - if (!widget) { - return; - } - - this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionId, widget.getInput(), CancellationToken.None) - .then((files) => { - if (!files?.length) { - return; - } - - const currentEditingSession = this.chatEditingService.currentEditingSessionObs.get(); - if (!currentEditingSession || currentEditingSession.chatSessionId !== widget.viewModel?.sessionId || currentEditingSession.entries.get().length) { - return; // Might have disposed while we were calculating - } - - // Pick up to 2 related files, or however many we can still fit in the working set - const maximumRelatedFiles = Math.min(2, this.chatEditingService.editingSessionFileLimit - widget.input.chatEditWorkingSetFiles.length); - const newSuggestions = new ResourceSet(); - for (const group of files) { - for (const file of group.files) { - if (newSuggestions.size >= maximumRelatedFiles) { - break; - } - newSuggestions.add(file.uri); - } - } - - // Remove the existing related file suggestions from the working set - const existingSuggestedEntriesToRemove: URI[] = []; - for (const entry of currentEditingSession.workingSet) { - if (entry[1].state === WorkingSetEntryState.Suggested && !newSuggestions.has(entry[0])) { - existingSuggestedEntriesToRemove.push(entry[0]); - } - } - currentEditingSession?.remove(WorkingSetEntryRemovalReason.Programmatic, ...existingSuggestedEntriesToRemove); - - // Add the new related file suggestions to the working set - for (const file of newSuggestions) { - currentEditingSession.addFileToWorkingSet(file, localize('relatedFile', "Suggested File"), WorkingSetEntryState.Suggested); - } - }) - .finally(() => { - this._currentRelatedFilesRetrievalOperation = undefined; - }); + if (!currentEditingSession) { + return; } + const workingSetEntries = currentEditingSession.entries.get(); + if (workingSetEntries.length > 0) { + // Do this only for the initial working set state + return; + } + + const widget = this.chatWidgetService.getWidgetBySessionId(currentEditingSession.chatSessionId); + if (!widget) { + return; + } + + this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionId, widget.getInput(), CancellationToken.None) + .then((files) => { + if (!files?.length) { + return; + } + + const currentEditingSession = this.chatEditingService.currentEditingSessionObs.get(); + if (!currentEditingSession || currentEditingSession.chatSessionId !== widget.viewModel?.sessionId || currentEditingSession.entries.get().length) { + return; // Might have disposed while we were calculating + } + + // Pick up to 2 related files, or however many we can still fit in the working set + const maximumRelatedFiles = Math.min(2, this.chatEditingService.editingSessionFileLimit - widget.input.chatEditWorkingSetFiles.length); + const newSuggestions = new ResourceMap<{ description: string; group: string }>(); + for (const group of files) { + for (const file of group.files) { + if (newSuggestions.size >= maximumRelatedFiles) { + break; + } + newSuggestions.set(file.uri, { group: group.group, description: file.description }); + } + } + + // Remove the existing related file suggestions from the working set + const existingSuggestedEntriesToRemove: URI[] = []; + for (const entry of currentEditingSession.workingSet) { + if (entry[1].state === WorkingSetEntryState.Suggested && !newSuggestions.has(entry[0])) { + existingSuggestedEntriesToRemove.push(entry[0]); + } + } + currentEditingSession?.remove(WorkingSetEntryRemovalReason.Programmatic, ...existingSuggestedEntriesToRemove); + + // Add the new related file suggestions to the working set + for (const [uri, data] of newSuggestions) { + currentEditingSession.addFileToWorkingSet(uri, localize('relatedFile', "{0} (Suggested)", data.description), WorkingSetEntryState.Suggested); + } + }) + .finally(() => { + this._currentRelatedFilesRetrievalOperation = undefined; + }); + } private _handleNewEditingSession() { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index e1f9bb59ebb..f799ee0c6fa 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -549,6 +549,7 @@ have to be updated for changes to the rules above, or to support more deeply nes display: flex; flex-direction: column; gap: 2px; + overflow: hidden; } .interactive-session .chat-editing-session .monaco-list-row .chat-collapsible-list-action-bar { @@ -576,7 +577,6 @@ have to be updated for changes to the rules above, or to support more deeply nes justify-content: space-between; gap: 6px; padding: 0 4px; - overflow: hidden; } .interactive-session .chat-editing-session .chat-editing-session-container .chat-editing-session-overview > .working-set-title { @@ -607,6 +607,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .chat-editing-session .chat-editing-session-toolbar-actions { margin: 3px 0px; + overflow: hidden; } .interactive-session .chat-editing-session .monaco-button { @@ -654,6 +655,13 @@ have to be updated for changes to the rules above, or to support more deeply nes cursor: pointer; display: flex; justify-content: start; +} + +.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary:first-child { + flex-shrink: 0; +} + +.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary:only-child { width: 100%; } @@ -698,6 +706,28 @@ have to be updated for changes to the rules above, or to support more deeply nes text-wrap: nowrap; } +.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button-dropdown.sidebyside-button { + align-items: center; + border-radius: 2px; +} +.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button-dropdown.sidebyside-button .monaco-button, +.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button-dropdown.sidebyside-button .monaco-button:hover { + border-right: 1px solid transparent; + background-color: unset; + padding: 0; +} +.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button-dropdown.sidebyside-button > .separator { + border-right: 1px solid transparent; + padding: 0 1px; + height: 22px; +} +.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button-dropdown.sidebyside-button:hover > .separator { + border-color: var(--vscode-input-border); +} +.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button-dropdown.sidebyside-button:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + .interactive-session .interactive-input-part.compact .chat-input-container { display: flex; justify-content: space-between; @@ -919,6 +949,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .codicon { padding-left: 4px; + font-size: 100% !important; } .interactive-session .chat-attached-context { @@ -1021,6 +1052,7 @@ have to be updated for changes to the rules above, or to support more deeply nes /* clamp to max 3 lines */ display: -webkit-box; + line-clamp: 3; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; @@ -1122,12 +1154,38 @@ have to be updated for changes to the rules above, or to support more deeply nes color: var(--vscode-chat-slashCommandForeground); } + .interactive-item-container .chat-resource-widget, .interactive-item-container .chat-agent-widget .monaco-button { border-radius: 4px; padding: 1px 3px; } +.interactive-item-container .chat-agent-command { + background-color: var(--vscode-chat-slashCommandBackground); + color: var(--vscode-chat-slashCommandForeground); + display: inline-flex; + align-items: center; + margin-right: 0.5ch; + border-radius: 4px; + padding: 0 0 0 3px; +} + +.interactive-item-container .chat-agent-command > .monaco-button { + display: flex; + align-self: stretch; + align-items: center; + cursor: pointer; + padding: 0 2px; + margin-left: 2px; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.interactive-item-container .chat-agent-command > .monaco-button:hover { + background: var(--vscode-toolbar-hoverBackground); +} + .interactive-item-container .chat-agent-widget .monaco-text-button { display: inline; border: none; diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css index 8f1b4235f12..52a1e9f7209 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css @@ -4,35 +4,50 @@ *--------------------------------------------------------------------------------------------*/ .chat-welcome-view .chat-setup-view { - text-align: initial; + text-align: center; p { width: 100%; } + .chat-features-container { + text-align: initial; + border-radius: 2px; + border: 1px solid var(--vscode-chat-requestBorder); + background-color: var(--vscode-chat-requestBackground); + } + + .chat-feature-container { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 10px 5px 10px; + } + + .chat-feature-container .codicon[class*='codicon-'] { + font-size: 20px; + } + .codicon[class*='codicon-'] { font-size: 13px; line-height: 1.4em; vertical-align: bottom; } - .monaco-button { + /** Dropdown Button */ + .monaco-button-dropdown { + width: 100%; + } + + .monaco-button-dropdown .monaco-text-button { + width: 100%; + } + + /** Single Button */ + p > .monaco-button { text-align: center; display: inline-block; width: 100%; padding: 4px 7px; } - - .checkbox-container { - display: flex; - } - - .checkbox-label { - flex-basis: fit-content; - cursor: pointer; - } - - .checkbox-label p { - display: inline; - } } diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index b9f7624d6cf..892607054e5 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -145,7 +145,7 @@ export class ChatViewWelcomePart extends Disposable { title.textContent = content.title; // Preview indicator - if (options?.location === ChatAgentLocation.EditingSession) { + if (options?.location === ChatAgentLocation.EditingSession && typeof content.message !== 'function') { const featureIndicator = dom.append(this.element, $('.chat-welcome-view-indicator')); featureIndicator.textContent = localize('preview', 'PREVIEW'); } diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index e3eed1531c9..05be0537726 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -44,6 +44,7 @@ export namespace ChatContextKeys { export const Setup = { canSignUp: new RawContextKey('chatSetupCanSignUp', false, { type: 'boolean', description: localize('chatSetupCanSignUp', "True when user can sign up to be a chat limited user.") }), + signedOut: new RawContextKey('chatSetupSignedOut', false, { type: 'boolean', description: localize('chatSetupSignedOut', "True when user is signed out.") }), entitled: new RawContextKey('chatSetupEntitled', false, { type: 'boolean', description: localize('chatSetupEntitled', "True when user is a chat entitled user.") }), limited: new RawContextKey('chatSetupLimited', false, { type: 'boolean', description: localize('chatSetupLimited', "True when user is a chat limited user.") }), diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 74f7d6cd270..e102d7525e9 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -7,7 +7,7 @@ import { CancellationToken, CancellationTokenSource } from '../../../../base/com import { Event } from '../../../../base/common/event.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../base/common/map.js'; -import { IObservable, ITransaction } from '../../../../base/common/observable.js'; +import { IObservable, IReader, ITransaction } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js'; import { TextEdit } from '../../../../editor/common/languages.js'; @@ -37,11 +37,7 @@ export interface IChatEditingService { readonly editingSessionFileLimit: number; startOrContinueEditingSession(chatSessionId: string): Promise; - getEditingSession(resource: URI): IChatEditingSession | null; - createSnapshot(requestId: string): void; - getSnapshotUri(requestId: string, uri: URI): URI | undefined; - restoreSnapshot(requestId: string | undefined): Promise; - + getOrRestoreEditingSession(): Promise; hasRelatedFilesProviders(): boolean; registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable; getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>; @@ -81,10 +77,16 @@ export interface IChatEditingSession { remove(reason: WorkingSetEntryRemovalReason, ...uris: URI[]): void; accept(...uris: URI[]): Promise; reject(...uris: URI[]): Promise; + getEntry(uri: URI): IModifiedFileEntry | undefined; + readEntry(uri: URI, reader?: IReader): IModifiedFileEntry | undefined; + + restoreSnapshot(requestId: string): Promise; + getSnapshotUri(requestId: string, uri: URI): URI | undefined; + /** * Will lead to this object getting disposed */ - stop(): Promise; + stop(clearState?: boolean): Promise; undoInteraction(): Promise; redoInteraction(): Promise; @@ -117,6 +119,7 @@ export interface IModifiedFileEntry { readonly state: IObservable; readonly isCurrentlyBeingModified: IObservable; readonly rewriteRatio: IObservable; + readonly maxLineNumber: IObservable; readonly diffInfo: IObservable; readonly lastModifyingRequestId: string; accept(transaction: ITransaction | undefined): Promise; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index cc30b06e5d7..ee0c9c98053 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -16,7 +16,7 @@ import { URI, UriComponents, UriDto, isUriComponents } from '../../../../base/co import { generateUuid } from '../../../../base/common/uuid.js'; import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/offsetRange.js'; import { IRange } from '../../../../editor/common/core/range.js'; -import { Location, TextEdit } from '../../../../editor/common/languages.js'; +import { Location, SymbolKind, TextEdit } from '../../../../editor/common/languages.js'; import { localize } from '../../../../nls.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, IChatWelcomeMessageContent, reviveSerializedAgent } from './chatAgents.js'; @@ -60,14 +60,23 @@ export interface IChatRequestPasteVariableEntry extends Omit { readonly kind: 'symbol'; readonly isDynamic: true; readonly value: Location; + readonly symbolKind: SymbolKind; } export interface ICommandResultVariableEntry extends Omit { diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 1f7578eeba4..0b2e220dbd4 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -77,6 +77,8 @@ export interface IChatRequestViewModel { readonly isHidden: boolean; readonly isComplete: boolean; readonly isCompleteAddedRequest: boolean; + readonly slashCommand: IChatAgentCommand | undefined; + readonly agentOrSlashCommandDetected: boolean; } export interface IChatResponseMarkdownRenderData { @@ -387,6 +389,14 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.isHidden; } + get slashCommand(): IChatAgentCommand | undefined { + return this._model.response?.slashCommand; + } + + get agentOrSlashCommandDetected(): boolean { + return this._model.response?.agentOrSlashCommandDetected ?? false; + } + currentRenderedHeight: number | undefined; constructor( diff --git a/src/vs/workbench/contrib/chat/common/languageModelStats.ts b/src/vs/workbench/contrib/chat/common/languageModelStats.ts index 290a6e3cd87..0a43eb15506 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelStats.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelStats.ts @@ -11,6 +11,7 @@ import { ExtensionIdentifier } from '../../../../platform/extensions/common/exte import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { localize } from '../../../../nls.js'; +import { Codicon } from '../../../../base/common/codicons.js'; export const ILanguageModelStatsService = createDecorator('ILanguageModelStatsService'); @@ -63,7 +64,7 @@ export class LanguageModelStatsService extends Disposable implements ILanguageMo } async update(model: string, extensionId: ExtensionIdentifier, agent: string | undefined, tokenCount: number | undefined): Promise { - await this.extensionFeaturesManagementService.getAccess(extensionId, 'languageModels'); + await this.extensionFeaturesManagementService.getAccess(extensionId, CopilotUsageExtensionFeatureId); // update model access this.addAccess(model, extensionId.value); @@ -160,11 +161,14 @@ export class LanguageModelStatsService extends Disposable implements ILanguageMo } } +export const CopilotUsageExtensionFeatureId = 'copilot'; Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ - id: 'languageModels', - label: localize('Language Models', "Language Models"), + id: CopilotUsageExtensionFeatureId, + label: localize('Language Models', "Copilot"), description: localize('languageModels', "Language models usage statistics of this extension."), + icon: Codicon.copilot, access: { canToggle: false }, + accessDataLabel: localize('chat', "chat"), }); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 2126b5ba952..4655198264d 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -45,7 +45,7 @@ import { renderViewTree } from './baseDebugView.js'; import { CONTINUE_ID, CONTINUE_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, PAUSE_ID, PAUSE_LABEL, RESTART_LABEL, RESTART_SESSION_ID, STEP_INTO_ID, STEP_INTO_LABEL, STEP_OUT_ID, STEP_OUT_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STOP_ID, STOP_LABEL } from './debugCommands.js'; import * as icons from './debugIcons.js'; import { createDisconnectMenuItemAction } from './debugToolBar.js'; -import { CALLSTACK_VIEW_ID, CONTEXT_CALLSTACK_ITEM_STOPPED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD, CONTEXT_CALLSTACK_SESSION_IS_ATTACH, CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, getStateLabel, IDebugModel, IDebugService, IDebugSession, IRawStoppedDetails, isFrameDeemphasized, IStackFrame, IThread, State } from '../common/debug.js'; +import { CALLSTACK_VIEW_ID, CONTEXT_CALLSTACK_FOCUSED, CONTEXT_CALLSTACK_ITEM_STOPPED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD, CONTEXT_CALLSTACK_SESSION_IS_ATTACH, CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, getStateLabel, IDebugModel, IDebugService, IDebugSession, IRawStoppedDetails, isFrameDeemphasized, IStackFrame, IThread, State } from '../common/debug.js'; import { StackFrame, Thread, ThreadAndSessionIds } from '../common/debugModel.js'; import { isSessionAttach } from '../common/debugUtils.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; @@ -283,6 +283,8 @@ export class CallStackView extends ViewPane { overrideStyles: this.getLocationBasedColors().listOverrideStyles }); + CONTEXT_CALLSTACK_FOCUSED.bindTo(this.tree.contextKeyService); + this.tree.setInput(this.debugService.getModel()); this._register(this.tree); this._register(this.tree.onDidOpen(async e => { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 9d26e02c3f4..d5387b10408 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -60,6 +60,7 @@ export const CONTEXT_CALLSTACK_ITEM_TYPE = new RawContextKey('callStackI export const CONTEXT_CALLSTACK_SESSION_IS_ATTACH = new RawContextKey('callStackSessionIsAttach', false, { type: 'boolean', description: nls.localize('callStackSessionIsAttach', "True when the session in the CALL STACK view is attach, false otherwise. Used internally for inline menus in the CALL STACK view.") }); export const CONTEXT_CALLSTACK_ITEM_STOPPED = new RawContextKey('callStackItemStopped', false, { type: 'boolean', description: nls.localize('callStackItemStopped', "True when the focused item in the CALL STACK is stopped. Used internaly for inline menus in the CALL STACK view.") }); export const CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD = new RawContextKey('callStackSessionHasOneThread', false, { type: 'boolean', description: nls.localize('callStackSessionHasOneThread', "True when the focused session in the CALL STACK view has exactly one thread. Used internally for inline menus in the CALL STACK view.") }); +export const CONTEXT_CALLSTACK_FOCUSED = new RawContextKey('callStackFocused', true, { type: 'boolean', description: nls.localize('callStackFocused', "True when the CALLSTACK view is focused, false otherwise.") }); export const CONTEXT_WATCH_ITEM_TYPE = new RawContextKey('watchItemType', undefined, { type: 'string', description: nls.localize('watchItemType', "Represents the item type of the focused element in the WATCH view. For example: 'expression', 'variable'") }); export const CONTEXT_CAN_VIEW_MEMORY = new RawContextKey('canViewMemory', undefined, { type: 'boolean', description: nls.localize('canViewMemory', "Indicates whether the item in the view has an associated memory refrence.") }); export const CONTEXT_BREAKPOINT_ITEM_TYPE = new RawContextKey('breakpointItemType', undefined, { type: 'string', description: nls.localize('breakpointItemType', "Represents the item type of the focused element in the BREAKPOINTS view. For example: 'breakpoint', 'exceptionBreakppint', 'functionBreakpoint', 'dataBreakpoint'") }); diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 308bf216205..dddffbe2517 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -422,8 +422,8 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { data.msgContainer.appendChild($('span', undefined, `${feature.label}: `)); data.msgContainer.appendChild($('span', undefined, ...renderLabelWithIcons(`$(${status.severity === Severity.Error ? errorIcon.id : warningIcon.id}) ${status.message}`))); } - if (accessData?.totalCount > 0) { - const element = $('span', undefined, `${nls.localize('requests count', "{0} Requests: {1} (Overall)", feature.label, accessData.totalCount)}${accessData.current ? nls.localize('session requests count', ", {0} (Session)", accessData.current.count) : ''}`); + if (accessData?.accessTimes.length > 0) { + const element = $('span', undefined, `${nls.localize('requests count', "{0} Usage: {1} Requests", feature.label, accessData.accessTimes.length)}${accessData.current ? nls.localize('session requests count', ", {0} Requests (Session)", accessData.current.accessTimes.length) : ''}`); if (accessData.current) { const title = nls.localize('requests count title', "Last request was {0}.", fromNow(accessData.current.lastAccessed, true, true)); data.elementDisposables.push(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, title)); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts b/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts index 1c04096e8a0..c529f2089f8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts @@ -36,15 +36,24 @@ import { IExtensionService } from '../../../services/extensions/common/extension import { Codicon } from '../../../../base/common/codicons.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { ResolvedKeybinding } from '../../../../base/common/keybindings.js'; -import { fromNow } from '../../../../base/common/date.js'; +import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; +import { foreground, chartAxis, chartGuide, chartLine } from '../../../../platform/theme/common/colorRegistry.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -class RuntimeStatusMarkdownRenderer extends Disposable implements IExtensionFeatureMarkdownRenderer { +interface IExtensionFeatureElementRenderer extends IExtensionFeatureRenderer { + type: 'element'; + render(manifest: IExtensionManifest): IRenderedData; +} + +class RuntimeStatusMarkdownRenderer extends Disposable implements IExtensionFeatureElementRenderer { static readonly ID = 'runtimeStatus'; - readonly type = 'markdown'; + readonly type = 'element'; constructor( @IExtensionService private readonly extensionService: IExtensionService, + @IOpenerService private readonly openerService: IOpenerService, + @IHoverService private readonly hoverService: IHoverService, @IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService, ) { super(); @@ -58,24 +67,25 @@ class RuntimeStatusMarkdownRenderer extends Disposable implements IExtensionFeat return !!manifest.main || !!manifest.browser; } - render(manifest: IExtensionManifest): IRenderedData { + render(manifest: IExtensionManifest): IRenderedData { const disposables = new DisposableStore(); const extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name)); - const emitter = disposables.add(new Emitter()); + const emitter = disposables.add(new Emitter()); disposables.add(this.extensionService.onDidChangeExtensionsStatus(e => { if (e.some(extension => ExtensionIdentifier.equals(extension, extensionId))) { - emitter.fire(this.getRuntimeStatusData(manifest)); + emitter.fire(this.createElement(manifest, disposables)); } })); - disposables.add(this.extensionFeaturesManagementService.onDidChangeAccessData(e => emitter.fire(this.getRuntimeStatusData(manifest)))); + disposables.add(this.extensionFeaturesManagementService.onDidChangeAccessData(e => emitter.fire(this.createElement(manifest, disposables)))); return { onDidChange: emitter.event, - data: this.getRuntimeStatusData(manifest), + data: this.createElement(manifest, disposables), dispose: () => disposables.dispose() }; } - private getRuntimeStatusData(manifest: IExtensionManifest): IMarkdownString { + private createElement(manifest: IExtensionManifest, disposables: DisposableStore): HTMLElement { + const container = $('.runtime-status'); const data = new MarkdownString(); const extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name)); const status = this.extensionService.getExtensionsStatus()[extensionId.value]; @@ -107,7 +117,7 @@ class RuntimeStatusMarkdownRenderer extends Disposable implements IExtensionFeat for (const feature of features) { const accessData = this.extensionFeaturesManagementService.getAccessData(extensionId, feature.id); if (accessData) { - data.appendMarkdown(`\n ### ${feature.label}\n\n`); + data.appendMarkdown(`\n ### ${localize('label', "{0} Usage", feature.label)}\n\n`); const status = accessData?.current?.status; if (status) { if (status?.severity === Severity.Error) { @@ -117,16 +127,187 @@ class RuntimeStatusMarkdownRenderer extends Disposable implements IExtensionFeat data.appendMarkdown(`$(${warningIcon.id}) ${status.message}\n\n`); } } - if (accessData?.totalCount) { - if (accessData.current) { - data.appendMarkdown(`${localize('last request', "Last Request: `{0}`", fromNow(accessData.current.lastAccessed, true, true))}\n\n`); - data.appendMarkdown(`${localize('requests count session', "Requests (Session) : `{0}`", accessData.current.count)}\n\n`); - } - data.appendMarkdown(`${localize('requests count total', "Requests (Overall): `{0}`", accessData.totalCount)}\n\n`); + } + } + this.renderMarkdown(data, container, disposables); + for (const feature of features) { + const accessData = this.extensionFeaturesManagementService.getAccessData(extensionId, feature.id); + if (accessData?.accessTimes.length) { + if (accessData?.accessTimes.length) { + const description = append(container, + $('.feature-chart-description', + undefined, + localize('chartDescription', "There were {0} {1} requests from this extension in the last 30 days.", accessData?.accessTimes.length, feature.accessDataLabel ?? feature.label))); + description.style.marginBottom = '8px'; + this.renderRequestsChart(container, accessData.accessTimes, disposables); } } } - return data; + return container; + } + + private renderMarkdown(markdown: IMarkdownString, container: HTMLElement, disposables: DisposableStore): void { + const { element, dispose } = renderMarkdown( + { + value: markdown.value, + isTrusted: markdown.isTrusted, + supportThemeIcons: true + }, + { + actionHandler: { + callback: (content) => this.openerService.open(content, { allowCommands: !!markdown.isTrusted }).catch(onUnexpectedError), + disposables + }, + }); + disposables.add(toDisposable(dispose)); + append(container, element); + } + + private renderRequestsChart(container: HTMLElement, accessTimes: Date[], disposables: DisposableStore): void { + const width = 450; + const height = 250; + const margin = { top: 0, right: 4, bottom: 20, left: 4 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const chartContainer = append(container, $('.feature-chart-container')); + chartContainer.style.position = 'relative'; + + const tooltip = append(chartContainer, $('.feature-chart-tooltip')); + tooltip.style.position = 'absolute'; + tooltip.style.width = '0px'; + tooltip.style.height = '0px'; + + let maxCount = 100; + const map = new Map(); + for (const accessTime of accessTimes) { + const day = `${accessTime.getDate()} ${accessTime.toLocaleString('default', { month: 'short' })}`; + map.set(day, (map.get(day) ?? 0) + 1); + maxCount = Math.max(maxCount, map.get(day)!); + } + + const now = new Date(); + type Point = { x: number; y: number; date: string; count: number }; + const points: Point[] = []; + for (let i = 0; i <= 30; i++) { + const date = new Date(now); + date.setDate(now.getDate() - (30 - i)); + const dateString = `${date.getDate()} ${date.toLocaleString('default', { month: 'short' })}`; + const count = map.get(dateString) ?? 0; + const x = (i / 30) * innerWidth; + const y = innerHeight - (count / maxCount) * innerHeight; + points.push({ x, y, date: dateString, count }); + } + + const chart = append(chartContainer, $('.feature-chart')); + const svg = append(chart, $.SVG('svg')); + svg.setAttribute('width', `${width}px`); + svg.setAttribute('height', `${height}px`); + svg.setAttribute('viewBox', `0 0 ${width} ${height}`); + + const g = $.SVG('g'); + g.setAttribute('transform', `translate(${margin.left},${margin.top})`); + svg.appendChild(g); + + const xAxisLine = $.SVG('line'); + xAxisLine.setAttribute('x1', '0'); + xAxisLine.setAttribute('y1', `${innerHeight}`); + xAxisLine.setAttribute('x2', `${innerWidth}`); + xAxisLine.setAttribute('y2', `${innerHeight}`); + xAxisLine.setAttribute('stroke', asCssVariable(chartAxis)); + xAxisLine.setAttribute('stroke-width', '1px'); + g.appendChild(xAxisLine); + + for (let i = 1; i <= 30; i += 7) { + const date = new Date(now); + date.setDate(now.getDate() - (30 - i)); + const dateString = `${date.getDate()} ${date.toLocaleString('default', { month: 'short' })}`; + const x = (i / 30) * innerWidth; + + // Add vertical line + const tick = $.SVG('line'); + tick.setAttribute('x1', `${x}`); + tick.setAttribute('y1', `${innerHeight}`); + tick.setAttribute('x2', `${x}`); + tick.setAttribute('y2', `${innerHeight + 10}`); + tick.setAttribute('stroke', asCssVariable(chartAxis)); + tick.setAttribute('stroke-width', '1px'); + g.appendChild(tick); + + const ruler = $.SVG('line'); + ruler.setAttribute('x1', `${x}`); + ruler.setAttribute('y1', `0`); + ruler.setAttribute('x2', `${x}`); + ruler.setAttribute('y2', `${innerHeight}`); + ruler.setAttribute('stroke', asCssVariable(chartGuide)); + ruler.setAttribute('stroke-width', '1px'); + g.appendChild(ruler); + + const xAxisDate = $.SVG('text'); + xAxisDate.setAttribute('x', `${x}`); + xAxisDate.setAttribute('y', `${height}`); // Adjusted y position to be within the SVG view port + xAxisDate.setAttribute('text-anchor', 'middle'); + xAxisDate.setAttribute('fill', asCssVariable(foreground)); + xAxisDate.setAttribute('font-size', '10px'); + xAxisDate.textContent = dateString; + g.appendChild(xAxisDate); + } + + const line = $.SVG('polyline'); + line.setAttribute('fill', 'none'); + line.setAttribute('stroke', asCssVariable(chartLine)); + line.setAttribute('stroke-width', `2px`); + line.setAttribute('points', points.map(p => `${p.x},${p.y}`).join(' ')); + g.appendChild(line); + + const highlightCircle = $.SVG('circle'); + highlightCircle.setAttribute('r', `4px`); + highlightCircle.style.display = 'none'; + g.appendChild(highlightCircle); + + const hoverDisposable = disposables.add(new MutableDisposable()); + const mouseMoveListener = (event: MouseEvent): void => { + const rect = svg.getBoundingClientRect(); + const mouseX = event.clientX - rect.left - margin.left; + + let closestPoint: Point | undefined; + let minDistance = Infinity; + + points.forEach(point => { + const distance = Math.abs(point.x - mouseX); + if (distance < minDistance) { + minDistance = distance; + closestPoint = point; + } + }); + + if (closestPoint) { + highlightCircle.setAttribute('cx', `${closestPoint.x}`); + highlightCircle.setAttribute('cy', `${closestPoint.y}`); + highlightCircle.style.display = 'block'; + tooltip.style.left = `${closestPoint.x + 24}px`; + tooltip.style.top = `${closestPoint.y + 14}px`; + hoverDisposable.value = this.hoverService.showHover({ + content: new MarkdownString(`${closestPoint.date}: ${closestPoint.count} requests`), + target: tooltip, + appearance: { + showPointer: true, + skipFadeInAnimation: true, + } + }); + } else { + hoverDisposable.value = undefined; + } + }; + svg.addEventListener('mousemove', mouseMoveListener); + disposables.add(toDisposable(() => svg.removeEventListener('mousemove', mouseMoveListener))); + + const mouseLeaveListener = () => { + highlightCircle.style.display = 'none'; + hoverDisposable.value = undefined; + }; + svg.addEventListener('mouseleave', mouseLeaveListener); + disposables.add(toDisposable(() => svg.removeEventListener('mouseleave', mouseLeaveListener))); } } @@ -438,6 +619,8 @@ class ExtensionFeatureView extends Disposable { this.renderMarkdownData(featureContentElement, renderer); } else if (renderer.type === 'markdown+table') { this.renderMarkdownAndTableData(featureContentElement, renderer); + } else if (renderer.type === 'element') { + this.renderElementData(featureContentElement, renderer); } } } @@ -549,6 +732,17 @@ class ExtensionFeatureView extends Disposable { } } + private renderElementData(container: HTMLElement, renderer: IExtensionFeatureElementRenderer): void { + const elementData = renderer.render(this.manifest); + if (elementData.onDidChange) { + this._register(elementData.onDidChange(data => { + clearNode(container); + container.appendChild(data); + })); + } + container.appendChild(elementData.data); + } + layout(height?: number, width?: number): void { this.layoutParticipants.forEach(p => p.layout(height, width)); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index e781968174e..25218191a2a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -13,11 +13,11 @@ import { EnablementState, IExtensionManagementServerService, IWorkbenchExtension import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType } from '../common/extensions.js'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType, EXTENSIONS_CATEGORY } from '../common/extensions.js'; import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from './extensionsActions.js'; import { ExtensionsInput } from '../common/extensionsInput.js'; import { ExtensionEditor } from './extensionEditor.js'; -import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer, BuiltInExtensionsContext, SearchMarketplaceExtensionsContext, RecommendedExtensionsContext, DefaultViewsContext, ExtensionsSortByContext, SearchHasTextContext } from './extensionsViewlet.js'; +import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer, BuiltInExtensionsContext, SearchMarketplaceExtensionsContext, RecommendedExtensionsContext, DefaultViewsContext, ExtensionsSortByContext, SearchHasTextContext, ExtensionsSearchValueContext } from './extensionsViewlet.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; import * as jsonContributionRegistry from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; import { ExtensionsConfigurationSchema, ExtensionsConfigurationSchemaId } from '../common/extensionsFileTemplate.js'; @@ -1192,7 +1192,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi this.registerExtensionAction({ id: `extensions.sort.${id}`, title, - precondition: precondition, + precondition: ContextKeyExpr.and(precondition, ContextKeyExpr.regex(ExtensionsSearchValueContext.key, /^@feature:/).negate()), menu: [{ id: extensionsSortSubMenu, when: ContextKeyExpr.or(CONTEXT_HAS_GALLERY, DefaultViewsContext), @@ -1805,7 +1805,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi this.registerExtensionAction({ id: 'workbench.extensions.action.addToWorkspaceRecommendations', title: localize2('workbench.extensions.action.addToWorkspaceRecommendations', "Add Extension to Workspace Recommendations"), - category: localize('extensions', "Extensions"), + category: EXTENSIONS_CATEGORY, menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), @@ -1828,7 +1828,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi this.registerExtensionAction({ id: 'workbench.extensions.action.addToWorkspaceFolderRecommendations', title: localize2('workbench.extensions.action.addToWorkspaceFolderRecommendations', "Add Extension to Workspace Folder Recommendations"), - category: localize('extensions', "Extensions"), + category: EXTENSIONS_CATEGORY, menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), @@ -1839,7 +1839,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi this.registerExtensionAction({ id: 'workbench.extensions.action.addToWorkspaceIgnoredRecommendations', title: localize2('workbench.extensions.action.addToWorkspaceIgnoredRecommendations', "Add Extension to Workspace Ignored Recommendations"), - category: localize('extensions', "Extensions"), + category: EXTENSIONS_CATEGORY, menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), @@ -1862,7 +1862,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi this.registerExtensionAction({ id: 'workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', title: localize2('workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', "Add Extension to Workspace Folder Ignored Recommendations"), - category: localize('extensions', "Extensions"), + category: EXTENSIONS_CATEGORY, menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), @@ -1873,7 +1873,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi this.registerExtensionAction({ id: ConfigureWorkspaceRecommendedExtensionsAction.ID, title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' }, - category: localize('extensions', "Extensions"), + category: EXTENSIONS_CATEGORY, menu: { id: MenuId.CommandPalette, when: WorkbenchStateContext.isEqualTo('workspace'), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 6c533cd8be7..2afa3e73bbf 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -12,10 +12,10 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { IPagedRenderer } from '../../../../base/browser/ui/list/listPaging.js'; import { Event } from '../../../../base/common/event.js'; -import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from '../common/extensions.js'; +import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService, IExtensionsViewState } from '../common/extensions.js'; import { ManageExtensionAction, ExtensionRuntimeStateAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ButtonWithDropDownExtensionAction, InstallDropdownAction, InstallingLabelAction, ButtonWithDropdownExtensionActionViewItem, DropDownExtensionAction, WebInstallAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction } from './extensionsActions.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; -import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionActivationStatusWidget, PreReleaseBookmarkWidget, extensionVerifiedPublisherIconColor, VerifiedPublisherWidget } from './extensionsWidgets.js'; +import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionRuntimeStatusWidget, PreReleaseBookmarkWidget, extensionVerifiedPublisherIconColor, VerifiedPublisherWidget } from './extensionsWidgets.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; @@ -29,11 +29,6 @@ import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/ac const EXTENSION_LIST_ELEMENT_HEIGHT = 72; -export interface IExtensionsViewState { - onFocus: Event; - onBlur: Event; -} - export interface ITemplateData { root: HTMLElement; element: HTMLElement; @@ -63,7 +58,7 @@ export type ExtensionListRendererOptions = { export class Renderer implements IPagedRenderer { constructor( - private extensionViewState: IExtensionsViewState, + private readonly extensionViewState: IExtensionsViewState, private readonly options: ExtensionListRendererOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, @INotificationService private readonly notificationService: INotificationService, @@ -148,7 +143,7 @@ export class Renderer implements IPagedRenderer { verifiedPublisherWidget, extensionHoverWidget, this.instantiationService.createInstance(SyncIgnoredWidget, syncIgnore), - this.instantiationService.createInstance(ExtensionActivationStatusWidget, activationStatus, true), + this.instantiationService.createInstance(ExtensionRuntimeStatusWidget, this.extensionViewState, activationStatus), this.instantiationService.createInstance(InstallCountWidget, installCount, true), this.instantiationService.createInstance(RatingsWidget, ratings, true), ]; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index 491d214b574..82f9f53d501 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -42,7 +42,7 @@ export class ExtensionsGridView extends Disposable { ) { super(); this.element = dom.append(parent, dom.$('.extensions-grid-view')); - this.renderer = this.instantiationService.createInstance(Renderer, { onFocus: Event.None, onBlur: Event.None }, { hoverOptions: { position() { return HoverPosition.BELOW; } } }); + this.renderer = this.instantiationService.createInstance(Renderer, { onFocus: Event.None, onBlur: Event.None, filters: {} }, { hoverOptions: { position() { return HoverPosition.BELOW; } } }); this.delegate = delegate; this.disposableStore = this._register(new DisposableStore()); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 18d5dd82d93..5fafa931213 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -88,6 +88,7 @@ const SearchUnsupportedWorkspaceExtensionsContext = new RawContextKey(' const SearchDeprecatedExtensionsContext = new RawContextKey('searchDeprecatedExtensions', false); export const RecommendedExtensionsContext = new RawContextKey('recommendedExtensions', false); const SortByUpdateDateContext = new RawContextKey('sortByUpdateDate', false); +export const ExtensionsSearchValueContext = new RawContextKey('extensionsSearchValue', ''); const REMOTE_CATEGORY: ILocalizedString = localize2({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"); @@ -478,24 +479,25 @@ export class ExtensionsViewletViewsContribution extends Disposable implements IW export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IExtensionsViewPaneContainer { - private defaultViewsContextKey: IContextKey; - private sortByContextKey: IContextKey; - private searchMarketplaceExtensionsContextKey: IContextKey; - private searchHasTextContextKey: IContextKey; - private sortByUpdateDateContextKey: IContextKey; - private installedExtensionsContextKey: IContextKey; - private searchInstalledExtensionsContextKey: IContextKey; - private searchRecentlyUpdatedExtensionsContextKey: IContextKey; - private searchExtensionUpdatesContextKey: IContextKey; - private searchOutdatedExtensionsContextKey: IContextKey; - private searchEnabledExtensionsContextKey: IContextKey; - private searchDisabledExtensionsContextKey: IContextKey; - private hasInstalledExtensionsContextKey: IContextKey; - private builtInExtensionsContextKey: IContextKey; - private searchBuiltInExtensionsContextKey: IContextKey; - private searchWorkspaceUnsupportedExtensionsContextKey: IContextKey; - private searchDeprecatedExtensionsContextKey: IContextKey; - private recommendedExtensionsContextKey: IContextKey; + private readonly extensionsSearchValueContextKey: IContextKey; + private readonly defaultViewsContextKey: IContextKey; + private readonly sortByContextKey: IContextKey; + private readonly searchMarketplaceExtensionsContextKey: IContextKey; + private readonly searchHasTextContextKey: IContextKey; + private readonly sortByUpdateDateContextKey: IContextKey; + private readonly installedExtensionsContextKey: IContextKey; + private readonly searchInstalledExtensionsContextKey: IContextKey; + private readonly searchRecentlyUpdatedExtensionsContextKey: IContextKey; + private readonly searchExtensionUpdatesContextKey: IContextKey; + private readonly searchOutdatedExtensionsContextKey: IContextKey; + private readonly searchEnabledExtensionsContextKey: IContextKey; + private readonly searchDisabledExtensionsContextKey: IContextKey; + private readonly hasInstalledExtensionsContextKey: IContextKey; + private readonly builtInExtensionsContextKey: IContextKey; + private readonly searchBuiltInExtensionsContextKey: IContextKey; + private readonly searchWorkspaceUnsupportedExtensionsContextKey: IContextKey; + private readonly searchDeprecatedExtensionsContextKey: IContextKey; + private readonly recommendedExtensionsContextKey: IContextKey; private searchDelayer: Delayer; private root: HTMLElement | undefined; @@ -528,6 +530,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); this.searchDelayer = new Delayer(500); + this.extensionsSearchValueContextKey = ExtensionsSearchValueContext.bindTo(contextKeyService); this.defaultViewsContextKey = DefaultViewsContext.bindTo(contextKeyService); this.sortByContextKey = ExtensionsSortByContext.bindTo(contextKeyService); this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService); @@ -791,6 +794,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.contextKeyService.bufferChangeEvents(() => { const isRecommendedExtensionsQuery = ExtensionsListView.isRecommendedExtensionsQuery(value); this.searchHasTextContextKey.set(value.trim() !== ''); + this.extensionsSearchValueContextKey.set(value); this.installedExtensionsContextKey.set(ExtensionsListView.isInstalledExtensionsQuery(value)); this.searchInstalledExtensionsContextKey.set(ExtensionsListView.isSearchInstalledExtensionsQuery(value)); this.searchRecentlyUpdatedExtensionsContextKey.set(ExtensionsListView.isSearchRecentlyUpdatedQuery(value) && !ExtensionsListView.isSearchExtensionUpdatesQuery(value)); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 8abcf780b3d..cf908c852e7 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -17,8 +17,8 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { append, $ } from '../../../../base/browser/dom.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { Delegate, Renderer, IExtensionsViewState } from './extensionsList.js'; -import { ExtensionState, IExtension, IExtensionsWorkbenchService, IWorkspaceRecommendedExtensionsView } from '../common/extensions.js'; +import { Delegate, Renderer } from './extensionsList.js'; +import { ExtensionResultsListFocused, ExtensionState, IExtension, IExtensionsViewState, IExtensionsWorkbenchService, IWorkspaceRecommendedExtensionsView } from '../common/extensions.js'; import { Query } from '../common/extensionQuery.js'; import { IExtensionService, toExtension } from '../../../services/extensions/common/extensions.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; @@ -62,6 +62,11 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js'; export const NONE_CATEGORY = 'none'; +type Message = { + readonly text: string; + readonly severity: Severity; +}; + class ExtensionsViewState extends Disposable implements IExtensionsViewState { private readonly _onFocus: Emitter = this._register(new Emitter()); @@ -72,6 +77,10 @@ class ExtensionsViewState extends Disposable implements IExtensionsViewState { private currentlyFocusedItems: IExtension[] = []; + filters: { + featureId?: string; + } = {}; + onFocusChange(extensions: IExtension[]): void { this.currentlyFocusedItems.forEach(extension => this._onBlur.fire(extension)); this.currentlyFocusedItems = extensions; @@ -88,6 +97,7 @@ export interface ExtensionsListViewOptions { interface IQueryResult { model: IPagedModel; + description?: string; readonly onDidChangeModel?: Event>; readonly disposables: DisposableStore; } @@ -119,6 +129,7 @@ export class ExtensionsListView extends ViewPane { private list: WorkbenchPagedList | null = null; private queryRequest: { query: string; request: CancelablePromise> } | null = null; private queryResult: IQueryResult | undefined; + private extensionsViewState: ExtensionsViewState | undefined; private readonly contextMenuActionRunner = this._register(new ActionRunner()); @@ -181,13 +192,13 @@ export class ExtensionsListView extends ViewPane { protected override renderBody(container: HTMLElement): void { super.renderBody(container); - const extensionsList = append(container, $('.extensions-list')); const messageContainer = append(container, $('.message-container')); const messageSeverityIcon = append(messageContainer, $('')); const messageBox = append(messageContainer, $('.message')); + const extensionsList = append(container, $('.extensions-list')); const delegate = new Delegate(); - const extensionsViewState = new ExtensionsViewState(); - const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState, { + this.extensionsViewState = new ExtensionsViewState(); + const renderer = this.instantiationService.createInstance(Renderer, this.extensionsViewState, { hoverOptions: { position: () => { const viewLocation = this.viewDescriptorService.getViewLocationById(this.id); @@ -216,10 +227,11 @@ export class ExtensionsListView extends ViewPane { overrideStyles: this.getLocationBasedColors().listOverrideStyles, openOnSingleClick: true }) as WorkbenchPagedList; + ExtensionResultsListFocused.bindTo(this.list.contextKeyService); this._register(this.list.onContextMenu(e => this.onContextMenu(e), this)); - this._register(this.list.onDidChangeFocus(e => extensionsViewState.onFocusChange(coalesce(e.elements)), this)); + this._register(this.list.onDidChangeFocus(e => this.extensionsViewState?.onFocusChange(coalesce(e.elements)), this)); this._register(this.list); - this._register(extensionsViewState); + this._register(this.extensionsViewState); this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => { this.openExtension(options.element!, { sideByside: options.sideBySide, ...options.editorOptions }); @@ -257,6 +269,9 @@ export class ExtensionsListView extends ViewPane { if (this.queryResult) { this.queryResult.disposables.dispose(); this.queryResult = undefined; + if (this.extensionsViewState) { + this.extensionsViewState.filters = {}; + } } const parsedQuery = Query.parse(query); @@ -277,7 +292,7 @@ export class ExtensionsListView extends ViewPane { try { this.queryResult = await this.query(parsedQuery, options, token); const model = this.queryResult.model; - this.setModel(model); + this.setModel(model, this.queryResult.description ? { text: this.queryResult.description, severity: Severity.Info } : undefined); if (this.queryResult.onDidChangeModel) { this.queryResult.disposables.add(this.queryResult.onDidChangeModel(model => { if (this.queryResult) { @@ -291,7 +306,7 @@ export class ExtensionsListView extends ViewPane { const model = new PagedModel([]); if (!isCancellationError(e)) { this.logService.error(e); - this.setModel(model, e); + this.setModel(model, this.getMessage(e)); } return this.list ? this.list.model : model; } @@ -392,7 +407,7 @@ export class ExtensionsListView extends ViewPane { private async queryLocal(query: Query, options: IQueryOptions): Promise { const local = await this.extensionsWorkbenchService.queryLocal(this.options.server); - let { extensions, canIncludeInstalledExtensions } = await this.filterLocal(local, this.extensionService.extensions, query, options); + let { extensions, canIncludeInstalledExtensions, description } = await this.filterLocal(local, this.extensionService.extensions, query, options); const disposables = new DisposableStore(); const onDidChangeModel = disposables.add(new Emitter>()); @@ -417,15 +432,17 @@ export class ExtensionsListView extends ViewPane { return { model: new PagedModel(extensions), + description, onDidChangeModel: onDidChangeModel.event, disposables }; } - private async filterLocal(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions): Promise<{ extensions: IExtension[]; canIncludeInstalledExtensions: boolean }> { + private async filterLocal(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions): Promise<{ extensions: IExtension[]; canIncludeInstalledExtensions: boolean; description?: string }> { const value = query.value; let extensions: IExtension[] = []; let canIncludeInstalledExtensions = true; + let description: string | undefined; if (/@builtin/i.test(value)) { extensions = this.filterBuiltinExtensions(local, query, options); @@ -461,10 +478,14 @@ export class ExtensionsListView extends ViewPane { } else if (/@feature:/i.test(query.value)) { - extensions = this.filterExtensionsByFeature(local, query, options); + const result = this.filterExtensionsByFeature(local, query); + if (result) { + extensions = result.extensions; + description = result.description; + } } - return { extensions, canIncludeInstalledExtensions }; + return { extensions, canIncludeInstalledExtensions, description }; } private filterBuiltinExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] { @@ -714,22 +735,33 @@ export class ExtensionsListView extends ViewPane { return this.sortExtensions(result, options); } - private filterExtensionsByFeature(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] { + private filterExtensionsByFeature(local: IExtension[], query: Query): { extensions: IExtension[]; description: string } | undefined { const value = query.value.replace(/@feature:/g, '').trim(); const featureId = value.split(' ')[0]; const feature = Registry.as(Extensions.ExtensionFeaturesRegistry).getExtensionFeature(featureId); if (!feature) { - return []; + return undefined; + } + if (this.extensionsViewState) { + this.extensionsViewState.filters.featureId = featureId; } const renderer = feature.renderer ? this.instantiationService.createInstance(feature.renderer) : undefined; try { - const result = local.filter(e => { + const result: [IExtension, number][] = []; + for (const e of local) { if (!e.local) { - return false; + continue; } - return renderer?.shouldRender(e.local.manifest) || this.extensionFeaturesManagementService.getAccessData(new ExtensionIdentifier(e.identifier.id), featureId); - }); - return this.sortExtensions(result, options); + const accessData = this.extensionFeaturesManagementService.getAccessData(new ExtensionIdentifier(e.identifier.id), featureId); + const shouldRender = renderer?.shouldRender(e.local.manifest); + if (accessData || shouldRender) { + result.push([e, accessData?.accessTimes.length ?? 0]); + } + } + return { + extensions: result.sort(([, a], [, b]) => b - a).map(([e]) => e), + description: localize('showingExtensionsForFeature', "Extensions using {0} in the last 30 days", feature.label) + }; } finally { renderer?.dispose(); } @@ -1087,13 +1119,13 @@ export class ExtensionsListView extends ViewPane { return new PagedModel(this.sortExtensions(installableRecommendations, options)); } - private setModel(model: IPagedModel, error?: any, donotResetScrollTop?: boolean) { + private setModel(model: IPagedModel, message?: Message, donotResetScrollTop?: boolean) { if (this.list) { this.list.model = new DelayedPagedModel(model); + this.updateBody(message); if (!donotResetScrollTop) { this.list.scrollTop = 0; } - this.updateBody(error); } if (this.badge) { this.badge.setCount(this.count()); @@ -1110,33 +1142,38 @@ export class ExtensionsListView extends ViewPane { } } - private updateBody(error?: any): void { + private updateBody(message?: Message): void { if (this.bodyTemplate) { const count = this.count(); this.bodyTemplate.extensionsList.classList.toggle('hidden', count === 0); - this.bodyTemplate.messageContainer.classList.toggle('hidden', count > 0); + this.bodyTemplate.messageContainer.classList.toggle('hidden', !message && count > 0); - if (count === 0 && this.isBodyVisible()) { - if (error) { - if (this.isOfflineError(error)) { - this.bodyTemplate.messageSeverityIcon.className = SeverityIcon.className(Severity.Warning); - this.bodyTemplate.messageBox.textContent = localize('offline error', "Unable to search the Marketplace when offline, please check your network connection."); - } else { - this.bodyTemplate.messageSeverityIcon.className = SeverityIcon.className(Severity.Error); - this.bodyTemplate.messageBox.textContent = localize('error', "Error while fetching extensions. {0}", getErrorMessage(error)); - } - } else { + if (this.isBodyVisible()) { + if (message) { + this.bodyTemplate.messageSeverityIcon.className = SeverityIcon.className(message.severity); + this.bodyTemplate.messageBox.textContent = message.text; + } else if (this.count() === 0) { this.bodyTemplate.messageSeverityIcon.className = ''; this.bodyTemplate.messageBox.textContent = localize('no extensions found', "No extensions found."); } - alert(this.bodyTemplate.messageBox.textContent); + if (this.bodyTemplate.messageBox.textContent) { + alert(this.bodyTemplate.messageBox.textContent); + } } } this.updateSize(); } + private getMessage(error: any): Message { + if (this.isOfflineError(error)) { + return { text: localize('offline error', "Unable to search the Marketplace when offline, please check your network connection."), severity: Severity.Warning }; + } else { + return { text: localize('error', "Error while fetching extensions. {0}", getErrorMessage(error)), severity: Severity.Error }; + } + } + private isOfflineError(error: Error): boolean { if (error instanceof ExtensionGalleryError) { return error.code === ExtensionGalleryErrorCode.Offline; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 503acfe26fd..729d5d96019 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -6,7 +6,7 @@ import './media/extensionsWidgets.css'; import * as semver from '../../../../base/common/semver/semver.js'; import { Disposable, toDisposable, DisposableStore, MutableDisposable, IDisposable } from '../../../../base/common/lifecycle.js'; -import { IExtension, IExtensionsWorkbenchService, IExtensionContainer, ExtensionState, ExtensionEditorTab } from '../common/extensions.js'; +import { IExtension, IExtensionsWorkbenchService, IExtensionContainer, ExtensionState, ExtensionEditorTab, IExtensionsViewState } from '../common/extensions.js'; import { append, $, reset, addDisposableListener, EventType, finalHandler } from '../../../../base/browser/dom.js'; import * as platform from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; @@ -43,6 +43,9 @@ import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defa import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js'; +import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension | null = null; @@ -481,12 +484,13 @@ export class SyncIgnoredWidget extends ExtensionWidget { } } -export class ExtensionActivationStatusWidget extends ExtensionWidget { +export class ExtensionRuntimeStatusWidget extends ExtensionWidget { constructor( + private readonly extensionViewState: IExtensionsViewState, private readonly container: HTMLElement, - private readonly small: boolean, @IExtensionService extensionService: IExtensionService, + @IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, ) { super(); @@ -495,6 +499,11 @@ export class ExtensionActivationStatusWidget extends ExtensionWidget { this.update(); } })); + this._register(extensionFeaturesManagementService.onDidChangeAccessData(e => { + if (this.extension && ExtensionIdentifier.equals(this.extension.identifier.id, e.extension)) { + this.update(); + } + })); } render(): void { @@ -504,21 +513,25 @@ export class ExtensionActivationStatusWidget extends ExtensionWidget { return; } - const extensionStatus = this.extensionsWorkbenchService.getExtensionRuntimeStatus(this.extension); - if (!extensionStatus || !extensionStatus.activationTimes) { - return; + if (this.extensionViewState.filters.featureId && this.extension.state === ExtensionState.Installed) { + const accessData = this.extensionFeaturesManagementService.getAllAccessDataForExtension(new ExtensionIdentifier(this.extension.identifier.id)).get(this.extensionViewState.filters.featureId); + const feature = Registry.as(Extensions.ExtensionFeaturesRegistry).getExtensionFeature(this.extensionViewState.filters.featureId); + if (feature?.icon && accessData) { + const featureAccessTimeElement = append(this.container, $('span.activationTime')); + featureAccessTimeElement.textContent = localize('feature access label', "{0} reqs", accessData.accessTimes.length); + const iconElement = append(this.container, $('span' + ThemeIcon.asCSSSelector(feature.icon))); + iconElement.style.paddingLeft = '4px'; + return; + } } - const activationTime = extensionStatus.activationTimes.codeLoadingTime + extensionStatus.activationTimes.activateCallTime; - if (this.small) { + const extensionStatus = this.extensionsWorkbenchService.getExtensionRuntimeStatus(this.extension); + if (extensionStatus?.activationTimes) { + const activationTime = extensionStatus.activationTimes.codeLoadingTime + extensionStatus.activationTimes.activateCallTime; append(this.container, $('span' + ThemeIcon.asCSSSelector(activationTimeIcon))); const activationTimeElement = append(this.container, $('span.activationTime')); activationTimeElement.textContent = `${activationTime}ms`; - } else { - const activationTimeElement = append(this.container, $('span.activationTime')); - activationTimeElement.textContent = `${localize('activation', "Activation time")}${extensionStatus.activationTimes.activationReason.startup ? ` (${localize('startup', "Startup")})` : ''} : ${activationTime}ms`; } - } } @@ -536,6 +549,7 @@ export class ExtensionHoverWidget extends ExtensionWidget { private readonly options: ExtensionHoverOptions, private readonly extensionStatusAction: ExtensionStatusAction, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService, @IHoverService private readonly hoverService: IHoverService, @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService, @@ -651,11 +665,12 @@ export class ExtensionHoverWidget extends ExtensionWidget { const preReleaseMessage = ExtensionHoverWidget.getPreReleaseMessage(this.extension); const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionRuntimeStatus(this.extension); + const extensionFeaturesAccessData = this.extensionFeaturesManagementService.getAllAccessDataForExtension(new ExtensionIdentifier(this.extension.identifier.id)); const extensionStatus = this.extensionStatusAction.status; const runtimeState = this.extension.runtimeState; const recommendationMessage = this.getRecommendationMessage(this.extension); - if (extensionRuntimeStatus || extensionStatus.length || runtimeState || recommendationMessage || preReleaseMessage) { + if (extensionRuntimeStatus || extensionFeaturesAccessData.size || extensionStatus.length || runtimeState || recommendationMessage || preReleaseMessage) { markdown.appendMarkdown(`---`); markdown.appendText(`\n`); @@ -681,6 +696,20 @@ export class ExtensionHoverWidget extends ExtensionWidget { } } + if (extensionFeaturesAccessData.size) { + const registry = Registry.as(Extensions.ExtensionFeaturesRegistry); + for (const [featureId, accessData] of extensionFeaturesAccessData) { + if (accessData?.accessTimes.length) { + const feature = registry.getExtensionFeature(featureId); + if (feature) { + markdown.appendMarkdown(localize('feature usage label', "{0} usage", feature.label)); + markdown.appendMarkdown(`: [${localize('total', "{0} {1} requests in last 30 days", accessData.accessTimes.length, feature.accessDataLabel ?? feature.label)}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.Features]))}`)})`); + markdown.appendText(`\n`); + } + } + } + } + for (const status of extensionStatus) { if (status.icon) { markdown.appendMarkdown(`$(${status.icon.id}) `); diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 32012238296..1e1067588d8 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -882,3 +882,12 @@ .extension-editor .subcontent.feature-contributions .extension-feature-content .feature-body .feature-body-content .feature-content.markdown .codicon { vertical-align: sub; } + +.extension-editor .subcontent.feature-contributions .extension-feature-content .feature-chart-container { + margin: 20px 0; +} + +.extension-editor .subcontent.feature-contributions .extension-feature-content .feature-chart-container > .feature-chart { + display: inline-block; + padding: 20px; +} diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 6207063200d..916628ddc60 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -21,8 +21,10 @@ import { MenuId } from '../../../../platform/actions/common/actions.js'; import { ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { Severity } from '../../../../platform/notification/common/notification.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; +import { localize2 } from '../../../../nls.js'; export const VIEWLET_ID = 'workbench.view.extensions'; +export const EXTENSIONS_CATEGORY = localize2('extensions', "Extensions"); export interface IExtensionsViewPaneContainer extends IViewPaneContainer { readonly searchValue: string | undefined; @@ -196,6 +198,14 @@ export interface IExtensionContainer extends IDisposable { update(): void; } +export interface IExtensionsViewState { + onFocus: Event; + onBlur: Event; + filters: { + featureId?: string; + }; +} + export class ExtensionContainers extends Disposable { constructor( @@ -240,6 +250,7 @@ export const LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID = 'workbench.exten // Context Keys export const HasOutdatedExtensionsContext = new RawContextKey('hasOutdatedExtensions', false); export const CONTEXT_HAS_GALLERY = new RawContextKey('hasGallery', false); +export const ExtensionResultsListFocused = new RawContextKey('extensionResultListFocused ', true); // Context Menu Groups export const THEME_ACTIONS_GROUP = '_theme_'; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 97cee3715ee..e77296ffed9 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -435,7 +435,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { const explorerLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); this._register(explorerLabels); - this.findProvider = this.instantiationService.createInstance(ExplorerFindProvider, () => this.tree); + this.findProvider = this.instantiationService.createInstance(ExplorerFindProvider, this.filter, () => this.tree); const updateWidth = (stat: ExplorerItem) => this.tree.updateWidth(stat); this.renderer = this.instantiationService.createInstance(FilesRenderer, container, explorerLabels, this.findProvider.highlightTree, updateWidth); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index b17d1a20cee..572f92c19b1 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -44,7 +44,7 @@ import { IWorkspaceFolderCreationData } from '../../../../../platform/workspaces import { findValidPasteFileTarget } from '../fileActions.js'; import { FuzzyScore, createMatches } from '../../../../../base/common/filters.js'; import { Emitter, Event, EventMultiplexer } from '../../../../../base/common/event.js'; -import { IAsyncDataTreeViewState, IAsyncFindProvider, IAsyncFindResultMetadata, IAsyncFindToggles, ITreeCompressionDelegate } from '../../../../../base/browser/ui/tree/asyncDataTree.js'; +import { IAsyncDataTreeViewState, IAsyncFindProvider, IAsyncFindResult, IAsyncFindToggles, ITreeCompressionDelegate } from '../../../../../base/browser/ui/tree/asyncDataTree.js'; import { ICompressibleTreeRenderer } from '../../../../../base/browser/ui/tree/objectTree.js'; import { ICompressedTreeNode } from '../../../../../base/browser/ui/tree/compressedObjectTreeModel.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; @@ -183,7 +183,8 @@ export class PhantomExplorerItem extends ExplorerItem { } interface FindHighlightLayer { - total: number; + childMatches: number; + isMatch: boolean; stats: { [statName: string]: FindHighlightLayer; }; @@ -191,6 +192,7 @@ interface FindHighlightLayer { interface IExplorerFindHighlightTree { get(item: ExplorerItem): number; + isMatch(item: ExplorerItem): boolean; } class ExplorerFindHighlightTree implements IExplorerFindHighlightTree { @@ -202,9 +204,21 @@ class ExplorerFindHighlightTree implements IExplorerFindHighlightTree { } get(item: ExplorerItem): number { + const result = this.find(item); + if (result === undefined) { + return 0; + } + + const { treeLayer, relPath } = result; + this._highlightedItems.set(relPath, item); + + return treeLayer.childMatches; + } + + private find(item: ExplorerItem): { treeLayer: FindHighlightLayer; relPath: string } | undefined { const rootLayer = this._tree.get(item.root.name); if (rootLayer === undefined) { - return 0; + return undefined; } const relPath = relativePath(item.root.resource, item.resource); @@ -215,15 +229,13 @@ class ExplorerFindHighlightTree implements IExplorerFindHighlightTree { let treeLayer = rootLayer; for (const segment of relPath.split('/')) { if (!treeLayer.stats[segment]) { - return 0; + return undefined; } treeLayer = treeLayer.stats[segment]; } - this._highlightedItems.set(relPath, item); - - return treeLayer.total; + return { treeLayer, relPath }; } add(resource: URI, root: ExplorerItem): void { @@ -233,18 +245,31 @@ class ExplorerFindHighlightTree implements IExplorerFindHighlightTree { } if (!this._tree.get(root.name)) { - this._tree.set(root.name, { total: 0, stats: {} }); + this._tree.set(root.name, { childMatches: 0, stats: {}, isMatch: false }); } let treeLayer = this._tree.get(root.name)!; - for (const stat of relPath.split('/').slice(0, -1)) { + for (const stat of relPath.split('/')) { if (!treeLayer.stats[stat]) { - treeLayer.stats[stat] = { total: 0, stats: {} }; + treeLayer.stats[stat] = { childMatches: 0, stats: {}, isMatch: false }; } treeLayer = treeLayer.stats[stat]; - treeLayer.total++; + treeLayer.childMatches++; } + + treeLayer.childMatches--; // the last segment is the file itself + treeLayer.isMatch = true; + } + + isMatch(item: ExplorerItem): boolean { + const result = this.find(item); + if (result === undefined) { + return false; + } + + const { treeLayer } = result; + return treeLayer.isMatch; } clear(): void { @@ -266,6 +291,7 @@ export class ExplorerFindProvider implements IAsyncFindProvider { } constructor( + private readonly filesFilter: FilesFilter, private readonly treeProvider: () => WorkbenchCompressibleAsyncDataTree, @ISearchService private readonly searchService: ISearchService, @IFileService private readonly fileService: IFileService, @@ -309,7 +335,7 @@ export class ExplorerFindProvider implements IAsyncFindProvider { } } - async find(pattern: string, toggles: IAsyncFindToggles, token: CancellationToken): Promise { + async find(pattern: string, toggles: IAsyncFindToggles, token: CancellationToken): Promise | undefined> { const promise = this.doFind(pattern, toggles, token); return await this.progressService.withProgress({ @@ -318,7 +344,7 @@ export class ExplorerFindProvider implements IAsyncFindProvider { }, _progress => promise); } - async doFind(pattern: string, toggles: IAsyncFindToggles, token: CancellationToken): Promise { + async doFind(pattern: string, toggles: IAsyncFindToggles, token: CancellationToken): Promise | undefined> { if (toggles.findMode === TreeFindMode.Highlight) { if (this.filterSessionStartState) { await this.endFilterSession(); @@ -357,7 +383,7 @@ export class ExplorerFindProvider implements IAsyncFindProvider { this.explorerFindActiveContextKey.set(true); } - async doFilterFind(pattern: string, matchType: TreeFindMatchType, token: CancellationToken): Promise { + async doFilterFind(pattern: string, matchType: TreeFindMatchType, token: CancellationToken): Promise | undefined> { if (!this.filterSessionStartState) { throw new Error('ExplorerFindProvider: no session state'); } @@ -366,7 +392,7 @@ export class ExplorerFindProvider implements IAsyncFindProvider { const searchResults = await this.getSearchResults(pattern, roots, matchType, token); if (token.isCancellationRequested) { - return {}; + return undefined; } this.clearPhantomElements(); @@ -378,7 +404,11 @@ export class ExplorerFindProvider implements IAsyncFindProvider { await tree.setInput(this.filterSessionStartState.input); const hitMaxResults = searchResults.some(({ hitMaxResults }) => hitMaxResults); - return { warningMessage: hitMaxResults ? localize('searchMaxResultsWarning', "The result set only contains a subset of all matches. Be more specific in your search to narrow down the results.") : undefined }; + return { + isMatch: (item: ExplorerItem) => item.isMarkedAsFiltered(), + matchCount: searchResults.reduce((acc, { files, directories }) => acc + files.length + directories.length, 0), + warningMessage: hitMaxResults ? localize('searchMaxResultsWarning', "The result set only contains a subset of all matches. Be more specific in your search to narrow down the results.") : undefined + }; } private addWorkspaceFilterResults(root: ExplorerItem, files: URI[], directories: URI[]): void { @@ -473,7 +503,7 @@ export class ExplorerFindProvider implements IAsyncFindProvider { this.highlightSessionStartState = { rootsWithProviders: new Set(roots) }; } - async doHighlightFind(pattern: string, matchType: TreeFindMatchType, token: CancellationToken): Promise { + async doHighlightFind(pattern: string, matchType: TreeFindMatchType, token: CancellationToken): Promise | undefined> { if (!this.highlightSessionStartState) { throw new Error('ExplorerFindProvider: no highlight session state'); } @@ -482,7 +512,7 @@ export class ExplorerFindProvider implements IAsyncFindProvider { const searchResults = await this.getSearchResults(pattern, roots, matchType, token); if (token.isCancellationRequested) { - return {}; + return undefined; } this.clearHighlights(); @@ -491,7 +521,11 @@ export class ExplorerFindProvider implements IAsyncFindProvider { } const hitMaxResults = searchResults.some(({ hitMaxResults }) => hitMaxResults); - return { warningMessage: hitMaxResults ? localize('searchMaxResultsWarning', "The result set only contains a subset of all matches. Be more specific in your search to narrow down the results.") : undefined }; + return { + isMatch: (item: ExplorerItem) => this.findHighlightTree.isMatch(item) || (this.findHighlightTree.get(item) > 0 && this.treeProvider().isCollapsed(item)), + matchCount: searchResults.reduce((acc, { files, directories }) => acc + files.length + directories.length, 0), + warningMessage: hitMaxResults ? localize('searchMaxResultsWarning', "The result set only contains a subset of all matches. Be more specific in your search to narrow down the results.") : undefined + }; } private addWorkspaceHighlightResults(root: ExplorerItem, resources: URI[]): void { @@ -515,7 +549,7 @@ export class ExplorerFindProvider implements IAsyncFindProvider { const firstParent = findFirstParent(resource, root); if (firstParent) { this.findHighlightTree.add(resource, root); - storeDirectories(firstParent); + storeDirectories(firstParent.parent!); } } @@ -588,7 +622,10 @@ export class ExplorerFindProvider implements IAsyncFindProvider { const fileResultResources = fileResults.results.map(result => result.resource); const directoryResources = getMatchingDirectoriesFromFiles(folderResults.results.map(result => result.resource), root, segmentMatchPattern); - return { explorerRoot: root, files: fileResultResources, directories: directoryResources, hitMaxResults: !!fileResults.limitHit || !!folderResults.limitHit }; + const filteredFileResources = fileResultResources.filter(resource => !this.filesFilter.isIgnored(resource, root.resource, false)); + const filteredDirectoryResources = directoryResources.filter(resource => !this.filesFilter.isIgnored(resource, root.resource, true)); + + return { explorerRoot: root, files: filteredFileResources, directories: filteredDirectoryResources, hitMaxResults: !!fileResults.limitHit || !!folderResults.limitHit }; } } @@ -1371,12 +1408,9 @@ export class FilesFilter implements ITreeFilter { // Hide those that match Hidden Patterns const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()); const globMatch = cached?.parsed(path.relative(stat.root.resource.path, stat.resource.path), stat.name, name => !!(stat.parent && stat.parent.getChild(name))); - // Small optimization to only traverse gitIgnore if the globMatch from fileExclude returned nothing - const ignoreFile = globMatch ? undefined : this.ignoreTreesPerRoot.get(stat.root.resource.toString())?.findSubstr(stat.resource); - const isIncludedInTraversal = ignoreFile?.isPathIncludedInTraversal(stat.resource.path, stat.isDirectory); - // Doing !undefined returns true and we want it to be false when undefined because that means it's not included in the ignore file - const isIgnoredByIgnoreFile = isIncludedInTraversal === undefined ? false : !isIncludedInTraversal; - if (isIgnoredByIgnoreFile || globMatch || stat.parent?.isExcluded) { + // Small optimization to only run isHiddenResource (traverse gitIgnore) if the globMatch from fileExclude returned nothing + const isHiddenResource = !!globMatch ? true : this.isIgnored(stat.resource, stat.root.resource, stat.isDirectory); + if (isHiddenResource || stat.parent?.isExcluded) { stat.isExcluded = true; const editors = this.editorService.visibleEditors; const editor = editors.find(e => e.resource && this.uriIdentityService.extUri.isEqualOrParent(e.resource, stat.resource)); @@ -1391,6 +1425,14 @@ export class FilesFilter implements ITreeFilter { return true; } + isIgnored(resource: URI, rootResource: URI, isDirectory: boolean): boolean { + const ignoreFile = this.ignoreTreesPerRoot.get(rootResource.toString())?.findSubstr(resource); + const isIncludedInTraversal = ignoreFile?.isPathIncludedInTraversal(resource.path, isDirectory); + + // Doing !undefined returns true and we want it to be false when undefined because that means it's not included in the ignore file + return isIncludedInTraversal === undefined ? false : !isIncludedInTraversal; + } + dispose(): void { dispose(this.toDispose); } diff --git a/src/vs/workbench/contrib/files/test/browser/explorerFindProvider.test.ts b/src/vs/workbench/contrib/files/test/browser/explorerFindProvider.test.ts index c4b21ae4321..cc796913bc4 100644 --- a/src/vs/workbench/contrib/files/test/browser/explorerFindProvider.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/explorerFindProvider.test.ts @@ -9,11 +9,11 @@ import { ICompressedTreeNode } from '../../../../../base/browser/ui/tree/compres import { ExplorerItem } from '../../common/explorerModel.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ITreeCompressionDelegate } from '../../../../../base/browser/ui/tree/asyncDataTree.js'; -import { ITreeNode, IAsyncDataSource } from '../../../../../base/browser/ui/tree/tree.js'; +import { ITreeNode, IAsyncDataSource, ITreeFilter, TreeFilterResult } from '../../../../../base/browser/ui/tree/tree.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { TestFileService, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { NullFilesConfigurationService } from '../../../../test/common/workbenchTestServices.js'; -import { ExplorerFindProvider } from '../../browser/views/explorerViewer.js'; +import { ExplorerFindProvider, FilesFilter } from '../../browser/views/explorerViewer.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IWorkbenchCompressibleAsyncDataTreeOptions, WorkbenchCompressibleAsyncDataTree } from '../../../../../platform/list/browser/listService.js'; import { IListAccessibilityProvider } from '../../../../../base/browser/ui/list/listWidget.js'; @@ -120,7 +120,13 @@ class CompressionDelegate implements ITreeCompressionDelegate { } } -suite('Files - ExplorerView', () => { +class TestFilesFilter implements ITreeFilter { + filter(): TreeFilterResult { return true; } + isIgnored(): boolean { return false; } + dispose() { } +} + +suite('Find Provider - ExplorerView', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); const fileService = new TestFileService(); @@ -196,6 +202,7 @@ suite('Files - ExplorerView', () => { const compressionDelegate = new CompressionDelegate(dataSource); const keyboardNavigationLabelProvider = new KeyboardNavigationLabelProvider(); const accessibilityProvider = new AccessibilityProvider(); + const filter = instantiationService.createInstance(TestFilesFilter) as unknown as FilesFilter; const options: IWorkbenchCompressibleAsyncDataTreeOptions = { identityProvider: new IdentityProvider(), keyboardNavigationLabelProvider, accessibilityProvider }; const tree = disposables.add(instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'test', container, new VirtualDelegate(), compressionDelegate, [new Renderer()], dataSource, options)); @@ -203,7 +210,7 @@ suite('Files - ExplorerView', () => { await tree.setInput(root); - const findProvider = instantiationService.createInstance(ExplorerFindProvider, () => tree); + const findProvider = instantiationService.createInstance(ExplorerFindProvider, filter, () => tree); findProvider.startSession(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 4ae0db4961b..e3ac8629904 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -17,6 +17,7 @@ import { StopWatch } from '../../../../base/common/stopwatch.js'; import { assertType } from '../../../../base/common/types.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { IPosition, Position } from '../../../../editor/common/core/position.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; import { ISelection, Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; @@ -591,8 +592,19 @@ export class InlineChatController implements IEditorContribution { const progressiveEditsClock = StopWatch.create(); const progressiveEditsQueue = new Queue(); - let next: State.WAIT_FOR_INPUT | State.SHOW_REQUEST | State.CANCEL | State.PAUSE | State.ACCEPT = State.WAIT_FOR_INPUT; + // disable typing and squiggles while streaming a reply + const origDeco = this._editor.getOption(EditorOption.renderValidationDecorations); + this._editor.updateOptions({ + renderValidationDecorations: 'off' + }); + store.add(toDisposable(() => { + this._editor.updateOptions({ + renderValidationDecorations: origDeco + }); + })); + + let next: State.WAIT_FOR_INPUT | State.SHOW_REQUEST | State.CANCEL | State.PAUSE | State.ACCEPT = State.WAIT_FOR_INPUT; store.add(Event.once(this._messages.event)(message => { this._log('state=_makeRequest) message received', message); this._chatService.cancelCurrentRequestForSession(chatModel.sessionId); @@ -804,9 +816,9 @@ export class InlineChatController implements IEditorContribution { this._log(err); } + this._resetWidget(); this._inlineChatSessionService.releaseSession(this._session); - this._resetWidget(); this._strategy?.dispose(); this._strategy = undefined; @@ -814,6 +826,9 @@ export class InlineChatController implements IEditorContribution { } private async[State.CANCEL]() { + + this._resetWidget(); + if (this._session) { // assertType(this._session); assertType(this._strategy); @@ -838,7 +853,6 @@ export class InlineChatController implements IEditorContribution { } } - this._resetWidget(); this._strategy?.dispose(); this._strategy = undefined; @@ -871,6 +885,10 @@ export class InlineChatController implements IEditorContribution { widgetPosition = this._session.wholeRange.trackedInitialRange.getStartPosition().delta(-1); } + if (initialRender && (this._editor.getOption(EditorOption.stickyScroll)).enabled) { + this._editor.revealLine(widgetPosition.lineNumber); // do NOT substract `this._editor.getOption(EditorOption.stickyScroll).maxLineCount` because the editor already does that + } + if (!headless) { if (this._ui.rawValue?.position) { this._ui.value.updatePositionAndHeight(widgetPosition); @@ -888,9 +906,7 @@ export class InlineChatController implements IEditorContribution { this._ctxVisible.reset(); this._ctxUserDidEdit.reset(); - if (this._ui.rawValue) { - this._ui.rawValue.hide(); - } + this._ui.rawValue?.hide(); // Return focus to the editor only if the current focus is within the editor widget if (this._editor.hasWidgetFocus()) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts index dfd271fa79b..d590221ac6a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts @@ -184,6 +184,12 @@ export class InlineChatHintsController extends Disposable implements IEditorCont const ghostCtrl = InlineCompletionsController.get(editor); + this._store.add(commandService.onWillExecuteCommand(e => { + if (e.commandId === _inlineChatActionId || e.commandId === ACTION_START) { + this.hide(); + } + })); + this._store.add(this._editor.onMouseDown(e => { if (e.target.type !== MouseTargetType.CONTENT_TEXT) { return; @@ -227,16 +233,19 @@ export class InlineChatHintsController extends Disposable implements IEditorCont const visible = this._visibilityObs.read(r);// || this._ctxMenuVisibleObs.read(r); const isEol = model.getLineMaxColumn(position.lineNumber) === position.column; - const isWhitespace = model.getLineLastNonWhitespaceColumn(position.lineNumber) === 0 && model.getValueLength() > 0; + const isWhitespace = model.getLineLastNonWhitespaceColumn(position.lineNumber) === 0 && model.getValueLength() > 0 && position.column > 1; - const shouldShow = (isWhitespace && _configurationService.getValue(InlineChatConfigKeys.LineEmptyHint)) - || (visible && isEol && _configurationService.getValue(InlineChatConfigKeys.LineSuffixHint)); - - if (!shouldShow) { - return undefined; + if (isWhitespace) { + return _configurationService.getValue(InlineChatConfigKeys.LineEmptyHint) + ? { isEol, isWhitespace, kb, position, model } + : undefined; } - return { isEol, isWhitespace, kb, position, model }; + if (visible && isEol && _configurationService.getValue(InlineChatConfigKeys.LineSuffixHint)) { + return { isEol, isWhitespace, kb, position, model }; + } + + return undefined; }); this._store.add(autorun(r => { @@ -249,24 +258,20 @@ export class InlineChatHintsController extends Disposable implements IEditorCont return; } - const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)?.fullName ?? localize('defaultTitle', "Chat"); + const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)?.name ?? localize('defaultTitle', "Chat"); const { position, isEol, isWhitespace, kb, model } = showData; const inlineClassName: string[] = ['inline-chat-hint']; let content: string; if (isWhitespace) { - content = '\u00a0' + localize('title2', "{0} to edit with {1}...", kb, agentName); + content = '\u00a0' + localize('title2', "{0} to edit with {1}", kb, agentName); } else if (isEol) { - content = '\u00a0' + localize('title1', "{0} to continue with {1}...", kb, agentName); + content = '\u00a0' + localize('title1', "{0} to continue with {1}", kb, agentName); } else { content = '\u200a' + kb + '\u200a'; inlineClassName.push('embedded'); } - if (decos.length === 0) { - inlineClassName.push('first'); - } - this._ctxShowingHint.set(true); decos.set([{ diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index 324c4b799f8..83d2d451dec 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -81,12 +81,11 @@ export class SessionWholeRange { } fixup(changes: readonly DetailedLineRangeMapping[]): void { - const newDeco: IModelDeltaDecoration[] = []; for (const { modified } of changes) { - const modifiedRange = modified.isEmpty - ? new Range(modified.startLineNumber, 1, modified.startLineNumber, this._textModel.getLineLength(modified.startLineNumber)) - : new Range(modified.startLineNumber, 1, modified.endLineNumberExclusive - 1, this._textModel.getLineLength(modified.endLineNumberExclusive - 1)); + const modifiedRange = this._textModel.validateRange(modified.isEmpty + ? new Range(modified.startLineNumber, 1, modified.startLineNumber, Number.MAX_SAFE_INTEGER) + : new Range(modified.startLineNumber, 1, modified.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER)); newDeco.push({ range: modifiedRange, options: SessionWholeRange._options }); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 9060badbfad..d82d9e99ddd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { $, Dimension, getActiveElement, getTotalHeight, h, reset, trackFocus } from '../../../../base/browser/dom.js'; -import { renderFormattedText } from '../../../../base/browser/formattedTextRenderer.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'; @@ -49,8 +48,7 @@ import { ChatWidget, IChatViewState, IChatWidgetLocationOptions } from '../../ch import { chatRequestBackground } from '../../chat/common/chatColors.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; import { IChatModel } from '../../chat/common/chatModel.js'; -import { chatSubcommandLeader } from '../../chat/common/chatParserTypes.js'; -import { ChatAgentVoteDirection, IChatSendRequestOptions, IChatService } from '../../chat/common/chatService.js'; +import { ChatAgentVoteDirection, IChatService } from '../../chat/common/chatService.js'; import { isResponseVM } from '../../chat/common/chatViewModel.js'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, inlineChatBackground, inlineChatForeground } from '../common/inlineChat.js'; import { HunkInformation, Session } from './inlineChatSession.js'; @@ -90,7 +88,6 @@ export class InlineChatWidget { h('div.status@status', [ h('div.label.info.hidden@infoLabel'), h('div.actions.hidden@toolbar1'), - h('div.rerun@rerun'), h('div.label.status.hidden@statusLabel'), h('div.actions.secondary.hidden@toolbar2'), ]), @@ -201,8 +198,6 @@ export class InlineChatWidget { ctxResponseSupportIssues.reset(); })); - const detectedAgentCmdStore = viewModelStore.add(new DisposableStore()); - viewModelStore.add(viewModel.onDidChange(() => { this._requestInProgress.set(viewModel.requestInProgress, undefined); @@ -216,37 +211,6 @@ export class InlineChatWidget { ctxResponseErrorFiltered.set((!!(isResponseVM(last) && last.errorDetails?.responseIsFiltered))); ctxResponseSupportIssues.set(isResponseVM(last) && (last.agent?.metadata.supportIssueReporting ?? false)); - if (isResponseVM(last) && last.agentOrSlashCommandDetected && last.slashCommand) { - this._elements.rerun.innerText = last.slashCommand.name; - this._elements.rerun.title = last.slashCommand.description; - - const msg = localize('usedAgentSlashCommand', "``{0}`` [[(rerun without)]]", `${chatSubcommandLeader}${last.slashCommand.name}`); - - reset(this._elements.rerun, renderFormattedText(msg, { - className: 'agentOrSlashCommandDetected', - inline: true, - renderCodeSegments: true, - actionHandler: { - disposables: detectedAgentCmdStore, - callback: (content) => { - const request = this._chatService.getSession(last.sessionId)?.getRequests().find(candidate => candidate.id === last.requestId); - if (request) { - const options: IChatSendRequestOptions = { - noCommandDetection: true, - attempt: request.attempt + 1, - location: location.location, - userSelectedModelId: this.chatWidget.input.currentLanguageModel - }; - this._chatService.resendRequest(request, options); - } - }, - } - })); - - } else { - reset(this._elements.rerun); - } - this._onDidChangeHeight.fire(); })); this._onDidChangeHeight.fire(); @@ -515,7 +479,6 @@ export class InlineChatWidget { this._chatWidget.saveState(); reset(this._elements.statusLabel); - reset(this._elements.rerun); this._elements.statusLabel.classList.toggle('hidden', true); this._elements.toolbar1.classList.add('hidden'); this._elements.toolbar2.classList.add('hidden'); @@ -557,13 +520,18 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { @IHoverService hoverService: IHoverService, @ILayoutService layoutService: ILayoutService ) { + const overflowWidgetsNode = layoutService.mainContainer.appendChild($('.inline-chat-overflow.monaco-editor')); super(location, { ...options, chatWidgetViewOptions: { ...options.chatWidgetViewOptions, - editorOverflowWidgetsDomNode: layoutService.mainContainer.appendChild($('.inline-chat-overflow.monaco-editor')) + editorOverflowWidgetsDomNode: overflowWidgetsNode } }, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService, hoverService); + + this._store.add(toDisposable(() => { + overflowWidgetsNode.remove(); + })); } // --- layout diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 267dd8dd858..ba1d884af6c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -89,6 +89,7 @@ export class InlineChatZoneWidget extends ZoneWidget { return isEqual(uri, editor.getModel()?.uri) && configurationService.getValue(InlineChatConfigKeys.Mode) === EditMode.Live; }, + renderDetectedCommandsWithRequest: true, } } }); @@ -154,8 +155,8 @@ export class InlineChatZoneWidget extends ZoneWidget { this._updatePadding(); const info = this.editor.getLayoutInfo(); - let width = info.contentWidth - info.verticalScrollbarWidth; - width = Math.min(850, width); + const width = info.contentWidth - info.verticalScrollbarWidth; + // width = Math.min(850, width); this._dimension = new Dimension(width, heightInPixel); this.widget.layout(this._dimension); diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index e2c67b0c97d..7ecb62c8cbc 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -87,7 +87,7 @@ .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-input-followups .interactive-session-followups { - margin: 2px 0 0 4px; + margin: 3px 0 0 4px; } .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .monaco-scrollable-element { @@ -361,32 +361,9 @@ .monaco-workbench .monaco-editor .inline-chat-hint { cursor: pointer; color: var(--vscode-editorGhostText-foreground); - background-image: linear-gradient(45deg, var(--vscode-editorGhostText-foreground), 95%, transparent); - background-clip: text; - -webkit-text-fill-color: transparent; } .monaco-workbench .monaco-editor .inline-chat-hint.embedded { border: 1px solid var(--vscode-editorSuggestWidget-border); border-radius: 3px; } - -@property --inline-chat-hint-progress { - syntax: ''; - initial-value: 33%; - inherits: false; -} - -@keyframes ltr { - 0% { - --inline-chat-hint-progress: 33%; - } - 100% { - --inline-chat-hint-progress: 95%; - } -} - -.monaco-workbench .monaco-editor .inline-chat-hint.first { - background-image: linear-gradient(45deg, var(--vscode-editorGhostText-foreground), var(--inline-chat-hint-progress), transparent); - animation: 75ms ltr ease-in forwards; -} diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 15d2b83cf5b..c3e9e82e228 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -65,12 +65,14 @@ Registry.as(Extensions.Configuration).registerConfigurat [InlineChatConfigKeys.LineEmptyHint]: { description: localize('emptyLineHint', "Whether empty lines show a hint to generate code with inline chat."), default: false, - type: 'boolean' + type: 'boolean', + tags: ['experimental'], }, [InlineChatConfigKeys.LineSuffixHint]: { description: localize('lineSuffixHint', "Whether a hint to complete a line with inline chat is shown."), default: true, - type: 'boolean' + type: 'boolean', + tags: ['experimental'], }, } }); 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 708a821ba7d..feaba4ca832 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -71,7 +71,7 @@ import { ITextModelService } from '../../../../../editor/common/services/resolve import { TextModelResolverService } from '../../../../services/textmodelResolver/common/textModelResolverService.js'; import { ChatInputBoxContentProvider } from '../../../chat/browser/chatEdinputInputContentProvider.js'; -suite('InteractiveChatController', function () { +suite('InlineChatController', function () { const agentData = { extensionId: nullExtensionDescription.identifier, @@ -290,7 +290,7 @@ suite('InteractiveChatController', function () { const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); assert.ok(session); - assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 10 /* line length */)); + assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 11 /* line length */)); editor.setSelection(new Range(2, 1, 2, 1)); editor.trigger('test', 'type', { text: 'a' }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts index ece78349224..992fcddbbaa 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts @@ -7,6 +7,7 @@ import { disposableTimeout, RunOnceScheduler } from '../../../../../../base/comm import { Disposable, dispose, IDisposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { language } from '../../../../../../base/common/platform.js'; import { localize } from '../../../../../../nls.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { themeColorFromId } from '../../../../../../platform/theme/common/themeService.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; @@ -15,7 +16,7 @@ import { ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebook import { registerNotebookContribution } from '../../notebookEditorExtensions.js'; import { cellStatusIconError, cellStatusIconSuccess } from '../../notebookEditorWidget.js'; import { errorStateIcon, executingStateIcon, pendingStateIcon, successStateIcon } from '../../notebookIcons.js'; -import { CellStatusbarAlignment, INotebookCellStatusBarItem, NotebookCellExecutionState, NotebookCellInternalMetadata } from '../../../common/notebookCommon.js'; +import { CellStatusbarAlignment, INotebookCellStatusBarItem, NotebookCellExecutionState, NotebookCellInternalMetadata, NotebookSetting } from '../../../common/notebookCommon.js'; import { INotebookCellExecution, INotebookExecutionStateService, NotebookExecutionType } from '../../../common/notebookExecutionStateService.js'; import { INotebookService } from '../../../common/notebookService.js'; import { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; @@ -227,17 +228,28 @@ class TimerCellStatusBarItem extends Disposable { private _deferredUpdate: IDisposable | undefined; + private _isVerbose: boolean; + constructor( private readonly _notebookViewModel: INotebookViewModel, private readonly _cell: ICellViewModel, @INotebookExecutionStateService private readonly _executionStateService: INotebookExecutionStateService, @INotebookService private readonly _notebookService: INotebookService, + @IConfigurationService private readonly _configurationService: IConfigurationService, ) { super(); + this._isVerbose = this._configurationService.getValue(NotebookSetting.cellExecutionTimeVerbosity) === 'verbose'; this._scheduler = this._register(new RunOnceScheduler(() => this._update(), TimerCellStatusBarItem.UPDATE_INTERVAL)); this._update(); this._register(this._cell.model.onDidChangeInternalMetadata(() => this._update())); + + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(NotebookSetting.cellExecutionTimeVerbosity)) { + this._isVerbose = this._configurationService.getValue(NotebookSetting.cellExecutionTimeVerbosity) === 'verbose'; + this._update(); + } + })); } private async _update() { @@ -290,8 +302,9 @@ class TimerCellStatusBarItem extends Disposable { let tooltip: IMarkdownString | undefined; + const lastExecution = new Date(endTime).toLocaleTimeString(language); + if (runtimeInformation) { - const lastExecution = new Date(endTime).toLocaleTimeString(language); const { renderDuration, executionDuration, timerDuration } = runtimeInformation; let renderTimes = ''; @@ -318,8 +331,12 @@ class TimerCellStatusBarItem extends Disposable { } + const executionText = this._isVerbose ? + localize('notebook.cell.statusBar.timerVerbose', "Last Execution: {0}, Duration: {1}", lastExecution, formatCellDuration(duration, false)) : + formatCellDuration(duration, false); + return { - text: formatCellDuration(duration, false), + text: executionText, alignment: CellStatusbarAlignment.Left, priority: Number.MAX_SAFE_INTEGER - 5, tooltip diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts index 9c3a2e66930..3b849314eee 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isEqual } from '../../../../../../base/common/resources.js'; import { Disposable, DisposableStore, dispose, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun, autorunWithStore, derived, observableFromEvent } from '../../../../../../base/common/observable.js'; import { IChatEditingService, ChatEditingSessionState } from '../../../../chat/common/chatEditingService.js'; @@ -32,6 +31,7 @@ import { splitLines } from '../../../../../../base/common/strings.js'; import { DefaultLineHeight } from '../../diff/diffElementViewModel.js'; import { INotebookOriginalCellModelFactory } from './notebookOriginalCellModelFactory.js'; import { DetailedLineRangeMapping } from '../../../../../../editor/common/diff/rangeMapping.js'; +import { isEqual } from '../../../../../../base/common/resources.js'; export class NotebookCellDiffDecorator extends DisposableStore { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts index 41b974bdc33..35de0be43b0 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts @@ -14,11 +14,12 @@ import { $ } from '../../../../../../base/browser/dom.js'; import { IChatEditingService, IModifiedFileEntry } from '../../../../chat/common/chatEditingService.js'; import { ACTIVE_GROUP, IEditorService } from '../../../../../services/editor/common/editorService.js'; import { Range } from '../../../../../../editor/common/core/range.js'; -import { autorunWithStore, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; +import { autorun, autorunWithStore, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; import { isEqual } from '../../../../../../base/common/resources.js'; import { CellDiffInfo } from '../../diff/notebookDiffViewModel.js'; import { INotebookDeletedCellDecorator } from './notebookCellDecorators.js'; import { AcceptAction, RejectAction } from '../../../../chat/browser/chatEditorActions.js'; +import { navigationBearingFakeActionId } from '../../../../chat/browser/chatEditorOverlay.js'; export class NotebookChatActionsOverlayController extends Disposable { constructor( @@ -54,8 +55,10 @@ export class NotebookChatActionsOverlayController extends Disposable { // Copied from src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts (until we unify these) export class NotebookChatActionsOverlay extends Disposable { private readonly focusedDiff = observableValue('focusedDiff', undefined); + private readonly toolbarNode: HTMLElement; + private added: boolean = false; constructor( - notebookEditor: INotebookEditor, + private readonly notebookEditor: INotebookEditor, entry: IModifiedFileEntry, cellDiffInfo: IObservable, nextEntry: IModifiedFileEntry, @@ -77,15 +80,21 @@ export class NotebookChatActionsOverlay extends Disposable { } })); - const toolbarNode = $('div'); - toolbarNode.classList.add('notebook-chat-editor-overlay-widget'); - notebookEditor.getDomNode().appendChild(toolbarNode); + this.toolbarNode = $('div'); + this.toolbarNode.classList.add('notebook-chat-editor-overlay-widget'); + this._register(toDisposable(() => this.hide())); - this._register(toDisposable(() => { - notebookEditor.getDomNode().removeChild(toolbarNode); + this._register(autorun(r => { + const diffs = cellDiffInfo.read(r); + if (diffs?.length) { + this.show(); + } else { + this.hide(); + } })); + const focusedDiff = this.focusedDiff; - const _toolbar = instaService.createInstance(MenuWorkbenchToolBar, toolbarNode, MenuId.ChatEditingEditorContent, { + const _toolbar = instaService.createInstance(MenuWorkbenchToolBar, this.toolbarNode, MenuId.ChatEditingEditorContent, { telemetrySource: 'chatEditor.overlayToolbar', hiddenItemStrategy: HiddenItemStrategy.Ignore, toolbarOptions: { @@ -95,6 +104,15 @@ export class NotebookChatActionsOverlay extends Disposable { menuOptions: { renderShortTitle: true }, actionViewItemProvider: (action, options) => { const that = this; + + if (action.id === navigationBearingFakeActionId) { + return new class extends ActionViewItem { + constructor() { + super(undefined, action, { ...options, icon: false, label: false, keybindingNotRenderedWithLabel: true }); + } + }; + } + if (action.id === AcceptAction.ID || action.id === RejectAction.ID) { return new class extends ActionViewItem { private readonly _reveal = this._store.add(new MutableDisposable()); @@ -158,6 +176,20 @@ export class NotebookChatActionsOverlay extends Disposable { this._register(_toolbar); } + private show() { + if (!this.added) { + this.notebookEditor.getDomNode().appendChild(this.toolbarNode); + this.added = true; + } + } + + private hide() { + if (this.added) { + this.notebookEditor.getDomNode().removeChild(this.toolbarNode); + this.added = false; + } + } + } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts index 03c38c2e40b..95fa1460ed8 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isEqual } from '../../../../../../base/common/resources.js'; import { Disposable, dispose, IReference, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun, derived, derivedWithStore, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; import { IChatEditingService, WorkingSetEntryState } from '../../../../chat/common/chatEditingService.js'; @@ -23,6 +22,7 @@ import { localize } from '../../../../../../nls.js'; import { registerNotebookContribution } from '../../notebookEditorExtensions.js'; import { InstantiationType, registerSingleton } from '../../../../../../platform/instantiation/common/extensions.js'; import { INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory } from './notebookOriginalCellModelFactory.js'; +import { Event } from '../../../../../../base/common/event.js'; export const ctxNotebookHasEditorModification = new RawContextKey('chat.hasNotebookEditorModifications', undefined, localize('chat.hasNotebookEditorModifications', "The current Notebook editor contains chat modifications")); @@ -62,7 +62,10 @@ class NotebookChatEditorController extends Disposable { this.insertedCellDecorator = this._register(instantiationService.createInstance(NotebookInsertedCellDecorator, notebookEditor)); const notebookModel = observableFromEvent(this.notebookEditor.onDidChangeModel, e => e); const originalModel = observableValue('originalModel', undefined); - const viewModelAttached = observableFromEvent(this.notebookEditor.onDidAttachViewModel, () => !!this.notebookEditor.getViewModel()); + // We need to render viewzones only when the viewmodel is attached (i.e. list view is ready). + // https://github.com/microsoft/vscode/issues/234718 + const readyToRenderViewzones = observableValue('viewModelAttached', false); + this._register(Event.once(this.notebookEditor.onDidAttachViewModel)(() => readyToRenderViewzones.set(true, undefined))); const onDidChangeVisibleRanges = debouncedObservable2(observableFromEvent(this.notebookEditor.onDidChangeVisibleRanges, () => this.notebookEditor.visibleRanges), 50); const decorators = new Map(); @@ -86,7 +89,7 @@ class NotebookChatEditorController extends Disposable { if (!model || !session) { return; } - return session.entries.read(r).find(e => isEqual(e.modifiedURI, model.uri)); + return session.readEntry(model.uri, r); }).recomputeInitiallyAndOnChange(this._store); @@ -195,8 +198,8 @@ class NotebookChatEditorController extends Disposable { const diffInfo = notebookDiffInfo.read(r); const modified = notebookModel.read(r); const original = originalModel.read(r); - const vmAttached = viewModelAttached.read(r); - if (!vmAttached || !entry || !modified || !original || !diffInfo) { + const ready = readyToRenderViewzones.read(r); + if (!ready || !entry || !modified || !original || !diffInfo) { return; } if (diffInfo && updatedDeletedInsertedDecoratorsOnceBefore && (diffInfo.modelVersion !== modified.versionId)) { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts index d2a13253484..6f3e33b7c9f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts @@ -91,7 +91,7 @@ export class NotebookModelSynchronizer extends Disposable { if (!session) { return; } - return session.entries.read(r).find(e => isEqual(e.modifiedURI, model.uri)); + return session.readEntry(model.uri, r); }).recomputeInitiallyAndOnChange(this._store); @@ -239,6 +239,7 @@ export class NotebookModelSynchronizer extends Disposable { const modifiedModel = (entry as ChatEditingModifiedFileEntry).modifiedModel; const content = modifiedModel.getValue(); await this.updateNotebook(VSBuffer.fromString(content), false); + this._diffInfo.set(undefined, undefined); } async getNotebookSerializer() { diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 4e05a378c96..f37a54c472f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -959,6 +959,16 @@ configurationRegistry.registerConfiguration({ default: 'visible', tags: ['notebookLayout'] }, + [NotebookSetting.cellExecutionTimeVerbosity]: { + description: nls.localize('notebook.cellExecutionTimeVerbosity.description', "Controls the verbosity of the cell execution time in the cell status bar."), + type: 'string', + enum: ['default', 'verbose'], + enumDescriptions: [ + nls.localize('notebook.cellExecutionTimeVerbosity.default.description', "The cell execution duration is visible, with advanced information in the hover tooltip."), + nls.localize('notebook.cellExecutionTimeVerbosity.verbose.description', "The cell last execution timestamp and duration are visible, with advanced information in the hover tooltip.")], + default: 'default', + tags: ['notebookLayout'] + }, [NotebookSetting.textDiffEditorPreview]: { description: nls.localize('notebook.diff.enablePreview.description', "Whether to use the enhanced text diff editor for notebook."), type: 'boolean', diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index 80d1a7ad448..b44f996ddc1 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -865,6 +865,16 @@ export class NotebookService extends Disposable implements INotebookService { return [...this.notebookProviderInfoStore]; } + hasSupportedNotebooks(resource: URI): boolean { + const contribution = this.notebookProviderInfoStore.getContributedNotebook(resource); + if (!contribution.length) { + return false; + } + return contribution.some(info => info.matches(resource) && + (info.priority === RegisteredEditorPriority.default || info.priority === RegisteredEditorPriority.exclusive) + ); + } + getContributedNotebookType(viewType: string): NotebookProviderInfo | undefined { return this.notebookProviderInfoStore.get(viewType); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 300b9209edc..d656f74241f 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -978,6 +978,7 @@ export const NotebookSetting = { cellToolbarLocation: 'notebook.cellToolbarLocation', cellToolbarVisibility: 'notebook.cellToolbarVisibility', showCellStatusBar: 'notebook.showCellStatusBar', + cellExecutionTimeVerbosity: 'notebook.cellExecutionTimeVerbosity', textDiffEditorPreview: 'notebook.diff.enablePreview', diffOverviewRuler: 'notebook.diff.overviewRuler', experimentalInsertToolbarAlignment: 'notebook.experimental.insertToolbarAlignment', diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 449a985c688..0970d96530a 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -91,6 +91,7 @@ export interface INotebookService { registerContributedNotebookType(viewType: string, data: INotebookContributionData): IDisposable; getContributedNotebookType(viewType: string): NotebookProviderInfo | undefined; getContributedNotebookTypes(resource?: URI): readonly NotebookProviderInfo[]; + hasSupportedNotebooks(resource: URI): boolean; getNotebookProviderResourceRoots(): URI[]; setToCopy(items: NotebookCellTextModel[], isCopy: boolean): void; diff --git a/src/vs/workbench/contrib/outline/browser/outline.ts b/src/vs/workbench/contrib/outline/browser/outline.ts index 6e2228a4356..863a93ec729 100644 --- a/src/vs/workbench/contrib/outline/browser/outline.ts +++ b/src/vs/workbench/contrib/outline/browser/outline.ts @@ -34,3 +34,4 @@ export const ctxFollowsCursor = new RawContextKey('outlineFollowsCursor export const ctxFilterOnType = new RawContextKey('outlineFiltersOnType', false); export const ctxSortMode = new RawContextKey('outlineSortMode', OutlineSortOrder.ByPosition); export const ctxAllCollapsed = new RawContextKey('outlineAllCollapsed', false); +export const ctxFocused = new RawContextKey('outlineFocused', true); diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 15091ceeec8..6149cd6de17 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -34,7 +34,7 @@ import { Event } from '../../../../base/common/event.js'; import { ITreeSorter } from '../../../../base/browser/ui/tree/tree.js'; import { AbstractTreeViewState, IAbstractTreeViewState, TreeFindMode } from '../../../../base/browser/ui/tree/abstractTree.js'; import { URI } from '../../../../base/common/uri.js'; -import { ctxAllCollapsed, ctxFilterOnType, ctxFollowsCursor, ctxSortMode, IOutlinePane, OutlineSortOrder } from './outline.js'; +import { ctxAllCollapsed, ctxFilterOnType, ctxFocused, ctxFollowsCursor, ctxSortMode, IOutlinePane, OutlineSortOrder } from './outline.js'; import { defaultProgressBarStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; @@ -270,6 +270,8 @@ export class OutlinePane extends ViewPane implements IOutlinePane { } ); + ctxFocused.bindTo(tree.contextKeyService); + // update tree, listen to changes const updateTree = () => { if (newOutline.isEmpty) { diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 5c2a8af08ec..18b04188fc6 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -526,6 +526,10 @@ margin-top: 9px; } +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .complex-object-edit-in-settings-button-container.hide { + display: none; +} + .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a, .settings-editor > .settings-body .settings-tree-container .setting-item-contents .edit-in-settings-button, .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a > code { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index d4d1b8458ac..7a1f0adcdaa 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -171,7 +171,8 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData const data = element.isConfigured ? { ...elementDefaultValue, ...elementScopeValue } : - elementDefaultValue; + element.hasPolicyValue ? element.scopeValue : + elementDefaultValue; const { objectProperties, objectPatternProperties, objectAdditionalProperties } = element.setting; const patternsAndSchemas = Object @@ -1246,6 +1247,7 @@ class SettingComplexObjectRenderer extends SettingComplexRenderer implements ITr showAddButton: false, isReadOnly: true, }); + template.button.parentElement?.classList.toggle('hide', dataElement.hasPolicyValue); super.renderValue(dataElement, template, onChange); } } diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 554ea526b66..3393ae598df 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -34,7 +34,7 @@ import { IDiffEditorOptions, EditorOption } from '../../../../editor/common/conf import { Action, IAction, ActionRunner } from '../../../../base/common/actions.js'; import { IActionBarOptions } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { basename, isEqual } from '../../../../base/common/resources.js'; +import { basename } from '../../../../base/common/resources.js'; import { MenuId, IMenuService, IMenu, MenuItemAction, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { ScrollType, IEditorContribution, IDiffEditorModel, IEditorModel, IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; @@ -1457,8 +1457,8 @@ export class DirtyDiffModel extends Disposable { } const uri = this._model.resource; - const session = this._chatEditingService.getEditingSession(uri); - if (session && session.entries.get().find(v => isEqual(v.modifiedURI, uri) && v.state.get() === WorkingSetEntryState.Modified)) { + const session = this._chatEditingService.currentEditingSession; + if (session && session.getEntry(uri)?.state.get() === WorkingSetEntryState.Modified) { // disable dirty diff when doing chat edits return Promise.resolve([]); } diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 4ba122fa370..b616cb2a26d 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -559,7 +559,9 @@ } .monaco-workbench .part.auxiliarybar > .title > .title-actions .action-label.scm-graph-repository-picker, -.monaco-workbench .part.auxiliarybar > .title > .title-actions .action-label.scm-graph-history-item-picker { +.monaco-workbench .part.auxiliarybar > .title > .title-actions .action-label.scm-graph-history-item-picker, +.monaco-workbench .part.panel > .title > .title-actions .action-label.scm-graph-repository-picker, +.monaco-workbench .part.panel > .title > .title-actions .action-label.scm-graph-history-item-picker { display: flex; } diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 8b786a6d407..8f6ff85d98b 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -40,11 +40,11 @@ import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/lis import { stripIcons } from '../../../../base/common/iconLabels.js'; import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; -import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { Action2, IMenuService, MenuId, MenuItemAction, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { Sequencer, Throttler } from '../../../../base/common/async.js'; import { URI } from '../../../../base/common/uri.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { ActionRunner, IAction, IActionRunner } from '../../../../base/common/actions.js'; +import { ActionRunner, IAction, IActionRunner, Separator, SubmenuAction } from '../../../../base/common/actions.js'; import { delta, groupBy } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IProgressService } from '../../../../platform/progress/common/progress.js'; @@ -63,6 +63,7 @@ import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hover import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { groupBy as groupBy2 } from '../../../../base/common/collections.js'; +import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; const PICK_REPOSITORY_ACTION_ID = 'workbench.scm.action.graph.pickRepository'; const PICK_HISTORY_ITEM_REFS_ACTION_ID = 'workbench.scm.action.graph.pickHistoryItemRefs'; @@ -1213,6 +1214,7 @@ export class SCMHistoryViewPane extends ViewPane { options: IViewPaneOptions, @ICommandService private readonly _commandService: ICommandService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IMenuService private readonly _menuService: IMenuService, @IProgressService private readonly _progressService: IProgressService, @IConfigurationService configurationService: IConfigurationService, @IContextMenuService contextMenuService: IContextMenuService, @@ -1570,14 +1572,46 @@ export class SCMHistoryViewPane extends ViewPane { return; } + const historyItemMenuActions = this._menuService.getMenuActions(MenuId.SCMChangesContext, this.scopedContextKeyService, { + arg: element.repository.provider, + shouldForwardArgs: true + }); + + const actions = getFlatContextMenuActions(historyItemMenuActions); + if (element.historyItemViewModel.historyItem.references?.length) { + actions.push(new Separator()); + } + + const that = this; + for (const ref of element.historyItemViewModel.historyItem.references ?? []) { + const contextKeyService = this.scopedContextKeyService.createOverlay([ + ['scmHistoryItemRef', ref.id] + ]); + + const historyItemRefMenuActions = this._menuService.getMenuActions(MenuId.SCMHistoryItemRefContext, contextKeyService); + const historyItemRefSubMenuActions = getFlatContextMenuActions(historyItemRefMenuActions) + .map(action => new class extends MenuItemAction { + constructor() { + super( + { id: action.id, title: action.label }, undefined, + { arg: element!.repository.provider, shouldForwardArgs: true }, + undefined, undefined, contextKeyService, that._commandService); + } + + override run(): Promise { + return super.run(element.historyItemViewModel.historyItem, ref.id); + } + }); + + if (historyItemRefSubMenuActions.length > 0) { + actions.push(new SubmenuAction(`scm.historyItemRef.${ref.id}`, ref.name, historyItemRefSubMenuActions)); + } + } + this.contextMenuService.showContextMenu({ contextKeyService: this.scopedContextKeyService, - menuId: MenuId.SCMChangesContext, - menuActionOptions: { - arg: element.repository.provider, - shouldForwardArgs: true - }, getAnchor: () => e.anchor, + getActions: () => actions, getActionsContext: () => element.historyItemViewModel.historyItem }); } diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 697012dbc5a..7dcd7c9d5a4 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -900,6 +900,8 @@ export class SearchView extends ViewPane { } })); + Constants.SearchContext.SearchResultListFocusedKey.bindTo(this.tree.contextKeyService); + this.tree.setInput(this.viewModel.searchResult); this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); const updateHasSomeCollapsible = () => this.toggleCollapseStateDelayer.trigger(() => this.hasSomeCollapsibleResultKey.set(this.hasSomeCollapsible())); @@ -970,13 +972,22 @@ export class SearchView extends ViewPane { e.browserEvent.preventDefault(); e.browserEvent.stopPropagation(); + const selection = this.tree.getSelection(); + let arg: any; + let context: any; + if (selection && selection.length > 0) { + arg = e.element; + context = selection; + } else { + context = e.element; + } this.contextMenuService.showContextMenu({ menuId: MenuId.SearchContext, - menuActionOptions: { shouldForwardArgs: true }, + menuActionOptions: { shouldForwardArgs: true, arg }, contextKeyService: this.contextKeyService, getAnchor: () => e.anchor, - getActionsContext: () => e.element, + getActionsContext: () => context, }); } diff --git a/src/vs/workbench/contrib/search/common/constants.ts b/src/vs/workbench/contrib/search/common/constants.ts index d3d521aed2d..4dff703e880 100644 --- a/src/vs/workbench/contrib/search/common/constants.ts +++ b/src/vs/workbench/contrib/search/common/constants.ts @@ -56,6 +56,7 @@ export const enum SearchCommandIds { export const SearchContext = { SearchViewVisibleKey: new RawContextKey('searchViewletVisible', true), SearchViewFocusedKey: new RawContextKey('searchViewletFocus', false), + SearchResultListFocusedKey: new RawContextKey('searchResultListFocused', true), InputBoxFocusedKey: new RawContextKey('inputBoxFocus', false), SearchInputBoxFocusedKey: new RawContextKey('searchInputBoxFocus', false), ReplaceInputBoxFocusedKey: new RawContextKey('replaceInputBoxFocus', false), diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 8666054c460..4143699e399 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -5,7 +5,6 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; -import { basename } from '../../../../../base/common/path.js'; import { URI } from '../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; @@ -177,7 +176,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return itemsWithModifiedLabels; } if (completions.resourceRequestConfig) { - const resourceCompletions = await this._resolveResources(completions.resourceRequestConfig, promptValue, cursorPosition); + const resourceCompletions = await this.resolveResources(completions.resourceRequestConfig, promptValue, cursorPosition); if (resourceCompletions) { itemsWithModifiedLabels.push(...resourceCompletions); } @@ -190,7 +189,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return results.filter(result => !!result).flat(); } - private async _resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number): Promise { + async resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number): Promise { const cwd = URI.revive(resourceRequestConfig.cwd); const foldersRequested = resourceRequestConfig.foldersRequested ?? false; const filesRequested = resourceRequestConfig.filesRequested ?? false; @@ -198,73 +197,49 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return; } + const fileStat = await this._fileService.resolve(cwd, { resolveSingleChildDescendants: true }); + if (!fileStat || !fileStat?.children) { + return; + } + const resourceCompletions: ITerminalCompletion[] = []; - - const parentDirPath = cwd.fsPath.split(resourceRequestConfig.pathSeparator).slice(0, -1).join(resourceRequestConfig.pathSeparator); - const dirToPrefixMap = new Map(); - - dirToPrefixMap.set(cwd, '.'); - dirToPrefixMap.set(cwd.with({ path: parentDirPath }), '..'); - const endsWithSpace = promptValue.substring(0, cursorPosition).endsWith(' '); let lastWord; if (endsWithSpace) { lastWord = ''; } else { - lastWord = promptValue.substring(0, cursorPosition).trim().split(' ').pop() ?? ''; - } - if (lastWord.includes(basename(cwd.fsPath))) { - lastWord = ''; + lastWord = promptValue.substring(0, cursorPosition).trim().split(' ').at(-1) ?? ''; } - // This breaks folder completions because has a different replacement index - // which results in replacement index being set to 0 - // const includeDirs = endsWithSpace || lastWord.match(/.*[\\\/]/); - // if (includeDirs) { - // resourceCompletions.push({ - // label: '.' + resourceRequestConfig.pathSeparator, - // kind: TerminalCompletionItemKind.Folder, - // isDirectory: true, - // replacementIndex: cursorPosition, - // replacementLength: 2 - // }); - // resourceCompletions.push({ - // label: '..' + resourceRequestConfig.pathSeparator, - // kind: TerminalCompletionItemKind.Folder, - // isDirectory: true, - // replacementIndex: cursorPosition, - // replacementLength: 3 - // }); - // } - - for (const [dir, prefix] of dirToPrefixMap) { - const fileStat = await this._fileService.resolve(dir, { resolveSingleChildDescendants: true }); - if (!fileStat || !fileStat?.children) { - return; + for (const stat of fileStat.children) { + let kind: TerminalCompletionItemKind | undefined; + if (foldersRequested && stat.isDirectory) { + kind = TerminalCompletionItemKind.Folder; } - - for (const stat of fileStat.children) { - let kind: TerminalCompletionItemKind | undefined; - if (foldersRequested && stat.isDirectory) { - kind = TerminalCompletionItemKind.Folder; - } - if (filesRequested && !stat.isDirectory && (stat.isFile || stat.resource.scheme === Schemas.file)) { - kind = TerminalCompletionItemKind.File; - } - if (kind === undefined) { - continue; - } - const isDirectory = kind === TerminalCompletionItemKind.Folder; - const label = isDirectory ? prefix + stat.resource.fsPath.replace(dir.fsPath, '') + resourceRequestConfig.pathSeparator : prefix + stat.resource.fsPath.replace(dir.fsPath, ''); - resourceCompletions.push({ - label, - kind, - isDirectory, - isFile: kind === TerminalCompletionItemKind.File, - replacementIndex: cursorPosition - lastWord.length, - replacementLength: label.length - }); + if (filesRequested && !stat.isDirectory && (stat.isFile || stat.resource.scheme === Schemas.file)) { + kind = TerminalCompletionItemKind.File; } + if (kind === undefined) { + continue; + } + const isDirectory = kind === TerminalCompletionItemKind.Folder; + const pathToResource = stat.resource.fsPath.replace(cwd.fsPath, ''); + let label = pathToResource; + if (isDirectory && !label.endsWith(resourceRequestConfig.pathSeparator)) { + label = label + resourceRequestConfig.pathSeparator; + } + const startsWithDot = lastWord && lastWord.startsWith('.'); + if (!startsWithDot) { + label = '.' + label; + } + resourceCompletions.push({ + label, + kind, + isDirectory, + isFile: kind === TerminalCompletionItemKind.File, + replacementIndex: promptValue[cursorPosition - 1] === ' ' ? cursorPosition : cursorPosition - 1, + replacementLength: label.length - lastWord.length > 0 ? label.length - lastWord.length : label.length, + }); } return resourceCompletions.length ? resourceCompletions : undefined; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts new file mode 100644 index 00000000000..d9e217ee398 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../../../base/common/uri.js'; +import { IFileStat, IFileStatWithMetadata, IResolveFileOptions, IResolveMetadataFileOptions } from '../../../../../../platform/files/common/files.js'; +import { TerminalCompletionService, TerminalCompletionItemKind, TerminalResourceRequestConfig } from '../../browser/terminalCompletionService.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import sinon from 'sinon'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { TestFileService } from '../../../../../test/browser/workbenchTestServices.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import assert from 'assert'; +import { isWindows } from '../../../../../../base/common/platform.js'; + +class TestFileService2 extends TestFileService { + private _children: IFileStat[] = []; + constructor() { + super(); + } + setChildren(children: IFileStat[]) { + this._children = children; + } + override async resolve(resource: URI, _options: IResolveMetadataFileOptions): Promise; + override async resolve(resource: URI, _options?: IResolveFileOptions): Promise; + override async resolve(resource: URI, _options?: IResolveFileOptions): Promise { + const result = await super.resolve(resource, _options); + result.children = this._children; + return result; + } +} + +suite('TerminalCompletionService', () => { + const pathSeparator = isWindows ? '\\' : '/'; + let fileService: TestFileService2; + let configurationService: IConfigurationService; + let terminalCompletionService: TerminalCompletionService; + const store = ensureNoDisposablesAreLeakedInTestSuite(); + setup(() => { + fileService = new TestFileService2(); + configurationService = new TestConfigurationService(); + terminalCompletionService = new TerminalCompletionService(configurationService, fileService); + store.add(terminalCompletionService); + store.add(fileService); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('resolveResources should return undefined', () => { + test('if cwd is not provided', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + pathSeparator + }; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3); + assert(!result); + }); + + test('if neither filesRequested nor foldersRequested are true', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + pathSeparator + }; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3); + assert(!result); + }); + }); + + suite('resolveResources should return folder completions', () => { + test('cd ', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + foldersRequested: true, + pathSeparator + }; + await fileService.createFolder(URI.parse('file:///test/')); + const childFolder = { resource: URI.parse('file:///test/folder1/'), name: 'folder1', isDirectory: true, isFile: false, isSymbolicLink: false, mtime: 0, size: 0, children: [] }; + const childFile = { resource: URI.parse('file:///test/file1.txt'), name: 'file1.txt', isDirectory: false, isFile: true, isSymbolicLink: true, mtime: 0, size: 0, children: [] }; + fileService.setChildren([childFolder, childFile]); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3); + assert(!!result); + assert(result.length === 1); + const label = `.${pathSeparator}folder1${pathSeparator}`; + assert.deepEqual(result![0], { + label, + kind: TerminalCompletionItemKind.Folder, + isDirectory: true, + isFile: false, + replacementIndex: 3, + replacementLength: label.length + }); + }); + test('cd .', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + foldersRequested: true, + pathSeparator + }; + await fileService.createFolder(URI.parse('file:///test/')); + const childFolder = { resource: URI.parse('file:///test/folder1/'), name: 'folder1', isDirectory: true, isFile: false, isSymbolicLink: false, mtime: 0, size: 0, children: [] }; + const childFile = { resource: URI.parse('file:///test/file1.txt'), name: 'file1.txt', isDirectory: false, isFile: true, isSymbolicLink: true, mtime: 0, size: 0, children: [] }; + fileService.setChildren([childFolder, childFile]); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd .', 4); + assert(!!result); + assert(result.length === 1); + const label = `${pathSeparator}folder1${pathSeparator}`; + assert.deepEqual(result![0], { + label, + kind: TerminalCompletionItemKind.Folder, + isDirectory: true, + isFile: false, + replacementIndex: 3, + replacementLength: label.length - 1 + }); + }); + test('cd ./', async () => { + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + foldersRequested: true, + pathSeparator + }; + await fileService.createFolder(URI.parse('file:///test/')); + const childFolder = { resource: URI.parse('file:///test/folder1/'), name: 'folder1', isDirectory: true, isFile: false, isSymbolicLink: false, mtime: 0, size: 0, children: [] }; + const childFile = { resource: URI.parse('file:///test/file1.txt'), name: 'file1.txt', isDirectory: false, isFile: true, isSymbolicLink: true, mtime: 0, size: 0, children: [] }; + fileService.setChildren([childFolder, childFile]); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5); + assert(!!result); + assert(result.length === 1); + const label = `${pathSeparator}folder1${pathSeparator}`; + assert.deepEqual(result![0], { + label, + kind: TerminalCompletionItemKind.Folder, + isDirectory: true, + isFile: false, + replacementIndex: 4, + replacementLength: label.length - 2 + }); + }); + }); +}); diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 494f0703bbe..f65839ebff5 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -1374,7 +1374,7 @@ class TestErrorContentWidget extends Disposable implements IContentWidget { let text: string; if (message.expected !== undefined && message.actual !== undefined) { - text = `${truncateMiddle(message.actual, 15)} != ${truncateMiddle(message.expected, 15)}`; + text = `${truncateMiddle(message.actual.replace(/\s+/g, ' '), 30)} != ${truncateMiddle(message.expected.replace(/\s+/g, ' '), 30)}`; } else { const msg = renderStringAsPlaintext(message.message); const lf = msg.indexOf('\n'); diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index a62b323be7c..b97f9201953 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -233,6 +233,7 @@ class LoadMoreCommand { export const TimelineFollowActiveEditorContext = new RawContextKey('timelineFollowActiveEditor', true, true); export const TimelineExcludeSources = new RawContextKey('timelineExcludeSources', '[]', true); +export const TimelineViewFocusedContext = new RawContextKey('timelineFocused', true); export class TimelinePane extends ViewPane { static readonly TITLE: ILocalizedString = localize2('timeline', "Timeline"); @@ -950,6 +951,8 @@ export class TimelinePane extends ViewPane { overrideStyles: this.getLocationBasedColors().listOverrideStyles, }); + TimelineViewFocusedContext.bindTo(this.tree.contextKeyService); + this._register(this.tree.onContextMenu(e => this.onContextMenu(this.commands, e))); this._register(this.tree.onDidChangeSelection(e => this.ensureValidItems())); this._register(this.tree.onDidOpen(e => { diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 683d0a92452..7b6dff93618 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -187,6 +187,12 @@ const apiMenus: IAPIMenu[] = [ description: localize('menus.historyItemContext', "The Source Control history item context menu"), proposed: 'contribSourceControlHistoryItemMenu' }, + { + key: 'scm/historyItemRef/context', + id: MenuId.SCMHistoryItemRefContext, + description: localize('menus.historyItemRefContext', "The Source Control history item reference context menu"), + proposed: 'contribSourceControlHistoryItemMenu' + }, { key: 'statusBar/remoteIndicator', id: MenuId.StatusBarRemoteIndicatorMenu, diff --git a/src/vs/workbench/services/driver/browser/driver.ts b/src/vs/workbench/services/driver/browser/driver.ts index d78e55aa973..ad689b9db6f 100644 --- a/src/vs/workbench/services/driver/browser/driver.ts +++ b/src/vs/workbench/services/driver/browser/driver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getClientArea, getTopLeftOffset } from '../../../../base/browser/dom.js'; +import { getClientArea, getTopLeftOffset, isHTMLDivElement, isHTMLTextAreaElement } from '../../../../base/browser/dom.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { language, locale } from '../../../../base/common/platform.js'; @@ -133,18 +133,36 @@ export class BrowserWindowDriver implements IWindowDriver { if (!element) { throw new Error(`Editor not found: ${selector}`); } + if (isHTMLDivElement(element)) { + // Edit context is enabled + const editContext = element.editContext; + if (!editContext) { + throw new Error(`Edit context not found: ${selector}`); + } + const selectionStart = editContext.selectionStart; + const selectionEnd = editContext.selectionEnd; + const event = new TextUpdateEvent('textupdate', { + updateRangeStart: selectionStart, + updateRangeEnd: selectionEnd, + text, + selectionStart: selectionStart + text.length, + selectionEnd: selectionStart + text.length, + compositionStart: 0, + compositionEnd: 0 + }); + editContext.dispatchEvent(event); + } else if (isHTMLTextAreaElement(element)) { + const start = element.selectionStart; + const newStart = start + text.length; + const value = element.value; + const newValue = value.substr(0, start) + text + value.substr(start); - const textarea = element as HTMLTextAreaElement; - const start = textarea.selectionStart; - const newStart = start + text.length; - const value = textarea.value; - const newValue = value.substr(0, start) + text + value.substr(start); + element.value = newValue; + element.setSelectionRange(newStart, newStart); - textarea.value = newValue; - textarea.setSelectionRange(newStart, newStart); - - const event = new Event('input', { 'bubbles': true, 'cancelable': true }); - textarea.dispatchEvent(event); + const event = new Event('input', { 'bubbles': true, 'cancelable': true }); + element.dispatchEvent(event); + } } async getTerminalBuffer(selector: string): Promise { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionFeatures.ts b/src/vs/workbench/services/extensionManagement/common/extensionFeatures.ts index a44288fd902..92d85391bc0 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionFeatures.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionFeatures.ts @@ -14,6 +14,7 @@ import Severity from '../../../../base/common/severity.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; import { ResolvedKeybinding } from '../../../../base/common/keybindings.js'; import { Color } from '../../../../base/common/color.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; export namespace Extensions { export const ExtensionFeaturesRegistry = 'workbench.registry.extensionFeatures'; @@ -55,7 +56,12 @@ export interface IExtensionFeatureMarkdownAndTableRenderer extends IExtensionFea export interface IExtensionFeatureDescriptor { readonly id: string; readonly label: string; + // label of the access data, if different from the feature title. + // This is useful when the feature is a part of a larger feature and the access data is not about the larger feature. + // This is shown in the access chart like "There were ${accessCount} ${accessLabel} requests from this extension". + readonly accessDataLabel?: string; readonly description?: string; + readonly icon?: ThemeIcon; readonly access: { readonly canToggle?: boolean; readonly requireUserConsent?: boolean; @@ -73,11 +79,11 @@ export interface IExtensionFeaturesRegistry { export interface IExtensionFeatureAccessData { readonly current?: { - readonly count: number; - readonly lastAccessed: number; + readonly accessTimes: Date[]; + readonly lastAccessed: Date; readonly status?: { readonly severity: Severity; readonly message: string }; }; - readonly totalCount: number; + readonly accessTimes: Date[]; } export const IExtensionFeaturesManagementService = createDecorator('IExtensionFeaturesManagementService'); @@ -92,6 +98,7 @@ export interface IExtensionFeaturesManagementService { getAccess(extension: ExtensionIdentifier, featureId: string, justification?: string): Promise; readonly onDidChangeAccessData: Event<{ readonly extension: ExtensionIdentifier; readonly featureId: string; readonly accessData: IExtensionFeatureAccessData }>; + getAllAccessDataForExtension(extension: ExtensionIdentifier): Map; getAccessData(extension: ExtensionIdentifier, featureId: string): IExtensionFeatureAccessData | undefined; setStatus(extension: ExtensionIdentifier, featureId: string, status: { readonly severity: Severity; readonly message: string } | undefined): void; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionFeaturesManagemetService.ts b/src/vs/workbench/services/extensionManagement/common/extensionFeaturesManagemetService.ts index 4eeb0851a4d..172241857f4 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionFeaturesManagemetService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionFeaturesManagemetService.ts @@ -47,6 +47,7 @@ class ExtensionFeaturesManagementService extends Disposable implements IExtensio super(); this.registry = Registry.as(Extensions.ExtensionFeaturesRegistry); this.extensionFeaturesState = this.loadState(); + this.garbageCollectOldRequests(); this._register(storageService.onDidChangeValue(StorageScope.PROFILE, FEATURES_STATE_KEY, this._store)(e => this.onDidStorageChange(e))); } @@ -59,7 +60,7 @@ class ExtensionFeaturesManagementService extends Disposable implements IExtensio if (isBoolean(isDisabled)) { return !isDisabled; } - const defaultExtensionAccess = feature.access.extensionsList?.[extension.value]; + const defaultExtensionAccess = feature.access.extensionsList?.[extension._lower]; if (isBoolean(defaultExtensionAccess)) { return defaultExtensionAccess; } @@ -109,7 +110,7 @@ class ExtensionFeaturesManagementService extends Disposable implements IExtensio const extensionDescription = this.extensionService.extensions.find(e => ExtensionIdentifier.equals(e.identifier, extension)); const confirmationResult = await this.dialogService.confirm({ title: localize('accessExtensionFeature', "Access '{0}' Feature", feature.label), - message: localize('accessExtensionFeatureMessage', "'{0}' extension would like to access the '{1}' feature.", extensionDescription?.displayName ?? extension.value, feature.label), + message: localize('accessExtensionFeatureMessage', "'{0}' extension would like to access the '{1}' feature.", extensionDescription?.displayName ?? extension._lower, feature.label), detail: justification ?? feature.description, custom: true, primaryButton: localize('allow', "Allow"), @@ -123,17 +124,29 @@ class ExtensionFeaturesManagementService extends Disposable implements IExtensio } } + const accessTime = new Date(); featureState.accessData.current = { - count: featureState.accessData.current?.count ? featureState.accessData.current?.count + 1 : 1, - lastAccessed: Date.now(), + accessTimes: [accessTime].concat(featureState.accessData.current?.accessTimes ?? []), + lastAccessed: accessTime, status: featureState.accessData.current?.status }; - featureState.accessData.totalCount = featureState.accessData.totalCount + 1; + featureState.accessData.accessTimes = (featureState.accessData.accessTimes ?? []).concat(accessTime); this.saveState(); this._onDidChangeAccessData.fire({ extension, featureId, accessData: featureState.accessData }); return true; } + getAllAccessDataForExtension(extension: ExtensionIdentifier): Map { + const result = new Map(); + const extensionState = this.extensionFeaturesState.get(extension._lower); + if (extensionState) { + for (const [featureId, featureState] of extensionState) { + result.set(featureId, featureState.accessData); + } + } + return result; + } + getAccessData(extension: ExtensionIdentifier, featureId: string): IExtensionFeatureAccessData | undefined { const feature = this.registry.getExtensionFeature(featureId); if (!feature) { @@ -149,26 +162,26 @@ class ExtensionFeaturesManagementService extends Disposable implements IExtensio } const featureState = this.getAndSetIfNotExistsExtensionFeatureState(extension, featureId); featureState.accessData.current = { - count: featureState.accessData.current?.count ?? 0, - lastAccessed: featureState.accessData.current?.lastAccessed ?? 0, + accessTimes: featureState.accessData.current?.accessTimes ?? [], + lastAccessed: featureState.accessData.current?.lastAccessed ?? new Date(), status }; this._onDidChangeAccessData.fire({ extension, featureId, accessData: this.getAccessData(extension, featureId)! }); } private getExtensionFeatureState(extension: ExtensionIdentifier, featureId: string): IExtensionFeatureState | undefined { - return this.extensionFeaturesState.get(extension.value)?.get(featureId); + return this.extensionFeaturesState.get(extension._lower)?.get(featureId); } private getAndSetIfNotExistsExtensionFeatureState(extension: ExtensionIdentifier, featureId: string): Mutable { - let extensionState = this.extensionFeaturesState.get(extension.value); + let extensionState = this.extensionFeaturesState.get(extension._lower); if (!extensionState) { extensionState = new Map(); - this.extensionFeaturesState.set(extension.value, extensionState); + this.extensionFeaturesState.set(extension._lower, extensionState); } let featureState = extensionState.get(featureId); if (!featureState) { - featureState = { accessData: { totalCount: 0 } }; + featureState = { accessData: { accessTimes: [] } }; extensionState.set(featureId, featureState); } return featureState; @@ -191,7 +204,7 @@ class ExtensionFeaturesManagementService extends Disposable implements IExtensio const newAccessData = this.getAccessData(extension, featureId); const oldAccessData = oldExtensionFeaturesState?.get(featureId)?.accessData; if (!equals(newAccessData, oldAccessData)) { - this._onDidChangeAccessData.fire({ extension, featureId, accessData: newAccessData ?? { totalCount: 0 } }); + this._onDidChangeAccessData.fire({ extension, featureId, accessData: newAccessData ?? { accessTimes: [] } }); } } } @@ -199,7 +212,7 @@ class ExtensionFeaturesManagementService extends Disposable implements IExtensio } private loadState(): Map> { - let data: IStringDictionary> = {}; + let data: IStringDictionary> = {}; const raw = this.storageService.get(FEATURES_STATE_KEY, StorageScope.PROFILE, '{}'); try { data = JSON.parse(raw); @@ -215,29 +228,49 @@ class ExtensionFeaturesManagementService extends Disposable implements IExtensio extensionFeatureState.set(featureId, { disabled: extensionFeature.disabled, accessData: { - totalCount: extensionFeature.accessCount + accessTimes: (extensionFeature.accessTimes ?? []).map(time => new Date(time)), } }); } - result.set(extensionId, extensionFeatureState); + result.set(extensionId.toLowerCase(), extensionFeatureState); } return result; } private saveState(): void { - const data: IStringDictionary> = {}; + const data: IStringDictionary> = {}; this.extensionFeaturesState.forEach((extensionState, extensionId) => { - const extensionFeatures: IStringDictionary<{ disabled?: boolean; accessCount: number }> = {}; + const extensionFeatures: IStringDictionary<{ disabled?: boolean; accessTimes: number[] }> = {}; extensionState.forEach((featureState, featureId) => { extensionFeatures[featureId] = { disabled: featureState.disabled, - accessCount: featureState.accessData.totalCount + accessTimes: featureState.accessData.accessTimes.map(time => time.getTime()), }; }); data[extensionId] = extensionFeatures; }); this.storageService.store(FEATURES_STATE_KEY, JSON.stringify(data), StorageScope.PROFILE, StorageTarget.USER); } + + private garbageCollectOldRequests(): void { + const now = new Date(); + const thirtyDaysAgo = new Date(now.setDate(now.getDate() - 30)); + let modified = false; + + for (const [, featuresStateMap] of this.extensionFeaturesState) { + for (const [, featureState] of featuresStateMap) { + const originalLength = featureState.accessData.accessTimes.length; + featureState.accessData.accessTimes = featureState.accessData.accessTimes.filter(accessTime => accessTime > thirtyDaysAgo); + if (featureState.accessData.accessTimes.length !== originalLength) { + modified = true; + } + } + } + + if (modified) { + this.saveState(); + } + } } registerSingleton(IExtensionFeaturesManagementService, ExtensionFeaturesManagementService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts index 7d00beb7569..7dc09d2e7e5 100644 --- a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts @@ -172,6 +172,9 @@ export class BrowserLifecycleService extends AbstractLifecycleService { joiners: () => [], // Unsupported in web token: CancellationToken.None, // Unsupported in web join(promise, joiner) { + if (typeof promise === 'function') { + promise(); + } logService.error(`[lifecycle] Long running operations during shutdown are unsupported in the web (id: ${joiner.id})`); }, force: () => { /* No-Op in web */ }, diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index a925cdd65bc..fd64b7cdeb1 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -12,6 +12,7 @@ import { launch as launchPlaywrightBrowser } from './playwrightBrowser'; import { PlaywrightDriver } from './playwrightDriver'; import { launch as launchPlaywrightElectron } from './playwrightElectron'; import { teardown } from './processes'; +import { Quality } from './application'; export interface LaunchOptions { codePath?: string; @@ -28,6 +29,7 @@ export interface LaunchOptions { readonly tracing?: boolean; readonly headless?: boolean; readonly browser?: 'chromium' | 'webkit' | 'firefox'; + readonly quality: Quality; } interface ICodeInstance { @@ -77,7 +79,7 @@ export async function launch(options: LaunchOptions): Promise { const { serverProcess, driver } = await measureAndLog(() => launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger); registerInstance(serverProcess, options.logger, 'server'); - return new Code(driver, options.logger, serverProcess); + return new Code(driver, options.logger, serverProcess, options.quality); } // Electron smoke tests (playwright) @@ -85,7 +87,7 @@ export async function launch(options: LaunchOptions): Promise { const { electronProcess, driver } = await measureAndLog(() => launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger); registerInstance(electronProcess, options.logger, 'electron'); - return new Code(driver, options.logger, electronProcess); + return new Code(driver, options.logger, electronProcess, options.quality); } } @@ -96,7 +98,8 @@ export class Code { constructor( driver: PlaywrightDriver, readonly logger: Logger, - private readonly mainProcess: cp.ChildProcess + private readonly mainProcess: cp.ChildProcess, + readonly quality: Quality ) { this.driver = new Proxy(driver, { get(target, prop) { diff --git a/test/automation/src/debug.ts b/test/automation/src/debug.ts index b7b7d427f4b..e2e227fc35e 100644 --- a/test/automation/src/debug.ts +++ b/test/automation/src/debug.ts @@ -9,6 +9,7 @@ import { Code, findElement } from './code'; import { Editors } from './editors'; import { Editor } from './editor'; import { IElement } from './driver'; +import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.debug"]'; const DEBUG_VIEW = `${VIEWLET}`; @@ -31,7 +32,8 @@ const CONSOLE_OUTPUT = `.repl .output.expression .value`; const CONSOLE_EVALUATION_RESULT = `.repl .evaluation-result.expression .value`; const CONSOLE_LINK = `.repl .value a.link`; -const REPL_FOCUSED = '.repl-input-wrapper .monaco-editor textarea'; +const REPL_FOCUSED_NATIVE_EDIT_CONTEXT = '.repl-input-wrapper .monaco-editor .native-edit-context'; +const REPL_FOCUSED_TEXTAREA = '.repl-input-wrapper .monaco-editor textarea'; export interface IStackFrame { name: string; @@ -127,8 +129,9 @@ export class Debug extends Viewlet { async waitForReplCommand(text: string, accept: (result: string) => boolean): Promise { await this.commands.runCommand('Debug: Focus on Debug Console View'); - await this.code.waitForActiveElement(REPL_FOCUSED); - await this.code.waitForSetValue(REPL_FOCUSED, text); + const selector = this.code.quality === Quality.Stable ? REPL_FOCUSED_TEXTAREA : REPL_FOCUSED_NATIVE_EDIT_CONTEXT; + await this.code.waitForActiveElement(selector); + await this.code.waitForSetValue(selector, text); // Wait for the keys to be picked up by the editor model such that repl evaluates what just got typed await this.editor.waitForEditorContents('debug:replinput', s => s.indexOf(text) >= 0); diff --git a/test/automation/src/editor.ts b/test/automation/src/editor.ts index 538866bfc06..dd616079565 100644 --- a/test/automation/src/editor.ts +++ b/test/automation/src/editor.ts @@ -6,6 +6,7 @@ import { References } from './peek'; import { Commands } from './workbench'; import { Code } from './code'; +import { Quality } from './application'; const RENAME_BOX = '.monaco-editor .monaco-editor.rename-box'; const RENAME_INPUT = `${RENAME_BOX} .rename-input`; @@ -78,10 +79,10 @@ export class Editor { async waitForEditorFocus(filename: string, lineNumber: number, selectorPrefix = ''): Promise { const editor = [selectorPrefix || '', EDITOR(filename)].join(' '); const line = `${editor} .view-lines > .view-line:nth-child(${lineNumber})`; - const textarea = `${editor} textarea`; + const editContext = `${editor} ${this._editContextSelector()}`; await this.code.waitAndClick(line, 1, 1); - await this.code.waitForActiveElement(textarea); + await this.code.waitForActiveElement(editContext); } async waitForTypeInEditor(filename: string, text: string, selectorPrefix = ''): Promise { @@ -92,14 +93,18 @@ export class Editor { await this.code.waitForElement(editor); - const textarea = `${editor} textarea`; - await this.code.waitForActiveElement(textarea); + const editContext = `${editor} ${this._editContextSelector()}`; + await this.code.waitForActiveElement(editContext); - await this.code.waitForTypeInEditor(textarea, text); + await this.code.waitForTypeInEditor(editContext, text); await this.waitForEditorContents(filename, c => c.indexOf(text) > -1, selectorPrefix); } + private _editContextSelector() { + return this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'; + } + async waitForEditorContents(filename: string, accept: (contents: string) => boolean, selectorPrefix = ''): Promise { const selector = [selectorPrefix || '', `${EDITOR(filename)} .view-lines`].join(' '); return this.code.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' '))); diff --git a/test/automation/src/editors.ts b/test/automation/src/editors.ts index b3a914ffff0..472385c8534 100644 --- a/test/automation/src/editors.ts +++ b/test/automation/src/editors.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Quality } from './application'; import { Code } from './code'; export class Editors { @@ -53,7 +54,7 @@ export class Editors { } async waitForActiveEditor(fileName: string, retryCount?: number): Promise { - const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] textarea`; + const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; return this.code.waitForActiveElement(selector, retryCount); } diff --git a/test/automation/src/extensions.ts b/test/automation/src/extensions.ts index 2a481f9fe76..c881e4fd8dc 100644 --- a/test/automation/src/extensions.ts +++ b/test/automation/src/extensions.ts @@ -8,6 +8,7 @@ import { Code } from './code'; import { ncp } from 'ncp'; import { promisify } from 'util'; import { Commands } from './workbench'; +import { Quality } from './application'; import path = require('path'); import fs = require('fs'); @@ -20,7 +21,7 @@ export class Extensions extends Viewlet { async searchForExtension(id: string): Promise { await this.commands.runCommand('Extensions: Focus on Extensions View', { exactLabelMatch: true }); - await this.code.waitForTypeInEditor('div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor textarea', `@id:${id}`); + await this.code.waitForTypeInEditor(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`, `@id:${id}`); await this.code.waitForTextContent(`div.part.sidebar div.composite.title h2`, 'Extensions: Marketplace'); let retrials = 1; diff --git a/test/automation/src/notebook.ts b/test/automation/src/notebook.ts index dff250027db..cd46cbdb0dd 100644 --- a/test/automation/src/notebook.ts +++ b/test/automation/src/notebook.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Quality } from './application'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; import { QuickInput } from './quickinput'; @@ -46,10 +47,10 @@ export class Notebook { await this.code.waitForElement(editor); - const textarea = `${editor} textarea`; - await this.code.waitForActiveElement(textarea); + const editContext = `${editor} ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; + await this.code.waitForActiveElement(editContext); - await this.code.waitForTypeInEditor(textarea, text); + await this.code.waitForTypeInEditor(editContext, text); await this._waitForActiveCellEditorContents(c => c.indexOf(text) > -1); } diff --git a/test/automation/src/scm.ts b/test/automation/src/scm.ts index 9f950f2b16a..6489badbe8a 100644 --- a/test/automation/src/scm.ts +++ b/test/automation/src/scm.ts @@ -6,9 +6,11 @@ import { Viewlet } from './viewlet'; import { IElement } from './driver'; import { findElement, findElements, Code } from './code'; +import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.scm"]'; -const SCM_INPUT = `${VIEWLET} .scm-editor textarea`; +const SCM_INPUT_NATIVE_EDIT_CONTEXT = `${VIEWLET} .scm-editor .native-edit-context`; +const SCM_INPUT_TEXTAREA = `${VIEWLET} .scm-editor textarea`; const SCM_RESOURCE = `${VIEWLET} .monaco-list-row .resource`; const REFRESH_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Refresh"]`; const COMMIT_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Commit"]`; @@ -44,7 +46,7 @@ export class SCM extends Viewlet { async openSCMViewlet(): Promise { await this.code.dispatchKeybinding('ctrl+shift+g'); - await this.code.waitForElement(SCM_INPUT); + await this.code.waitForElement(this._editContextSelector()); } async waitForChange(name: string, type?: string): Promise { @@ -71,9 +73,13 @@ export class SCM extends Viewlet { } async commit(message: string): Promise { - await this.code.waitAndClick(SCM_INPUT); - await this.code.waitForActiveElement(SCM_INPUT); - await this.code.waitForSetValue(SCM_INPUT, message); + await this.code.waitAndClick(this._editContextSelector()); + await this.code.waitForActiveElement(this._editContextSelector()); + await this.code.waitForSetValue(this._editContextSelector(), message); await this.code.waitAndClick(COMMIT_COMMAND); } + + private _editContextSelector(): string { + return this.code.quality === Quality.Stable ? SCM_INPUT_TEXTAREA : SCM_INPUT_NATIVE_EDIT_CONTEXT; + } } diff --git a/test/automation/src/settings.ts b/test/automation/src/settings.ts index 68401eb0eda..8cf221b1487 100644 --- a/test/automation/src/settings.ts +++ b/test/automation/src/settings.ts @@ -7,8 +7,10 @@ import { Editor } from './editor'; import { Editors } from './editors'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; +import { Quality } from './application'; -const SEARCH_BOX = '.settings-editor .suggest-input-container .monaco-editor textarea'; +const SEARCH_BOX_NATIVE_EDIT_CONTEXT = '.settings-editor .suggest-input-container .monaco-editor .native-edit-context'; +const SEARCH_BOX_TEXTAREA = '.settings-editor .suggest-input-container .monaco-editor textarea'; export class SettingsEditor { constructor(private code: Code, private editors: Editors, private editor: Editor, private quickaccess: QuickAccess) { } @@ -57,13 +59,13 @@ export class SettingsEditor { async openUserSettingsUI(): Promise { await this.quickaccess.runCommand('workbench.action.openSettings2'); - await this.code.waitForActiveElement(SEARCH_BOX); + await this.code.waitForActiveElement(this._editContextSelector()); } async searchSettingsUI(query: string): Promise { await this.openUserSettingsUI(); - await this.code.waitAndClick(SEARCH_BOX); + await this.code.waitAndClick(this._editContextSelector()); if (process.platform === 'darwin') { await this.code.dispatchKeybinding('cmd+a'); } else { @@ -71,7 +73,11 @@ export class SettingsEditor { } await this.code.dispatchKeybinding('Delete'); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => !results || (results?.length === 1 && !results[0].textContent)); - await this.code.waitForTypeInEditor('.settings-editor .suggest-input-container .monaco-editor textarea', query); + await this.code.waitForTypeInEditor(this._editContextSelector(), query); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => results?.length === 1 && results[0].textContent.includes('Found')); } + + private _editContextSelector() { + return this.code.quality === Quality.Stable ? SEARCH_BOX_TEXTAREA : SEARCH_BOX_NATIVE_EDIT_CONTEXT; + } } diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index 9dd1b2174ec..7c28a98930c 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -211,7 +211,7 @@ async function loadTests(opts) { ]); const _allowedSuitesWithOutput = new Set([ - 'InteractiveChatController' + 'InlineChatController' ]); let _testsWithUnexpectedOutput = false;