From eee2ff0fdcb422608b5181338e6204cdb5e311cd Mon Sep 17 00:00:00 2001 From: Jonathan Date: Mon, 15 Jul 2024 14:58:59 -0700 Subject: [PATCH 01/38] Better guidance for output / devtools location (#4021) This change addresses some feedback we've been getting that people who report issues don't know what the Developer Tools console is or how to access it. The "output" can also be unclear but is a more nuanced topic so I've linked it to our troubleshooting page. ### QA Notes N/A, issue template change only --- .github/ISSUE_TEMPLATE/bug_report.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3814aa483a8..d33caef698b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -16,5 +16,10 @@ labels: ['bug'] ## What did you expect to happen? -## Were there any error messages in the output or Developer Tools console? +## Were there any error messages in the Output panel or Developer Tools console? + + From 6d8435709d94ae00187218988b2e7ee3e6eeaf82 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Tue, 16 Jul 2024 09:49:11 +0200 Subject: [PATCH 02/38] Add issue template for Ark (#4005) Closes #3960 --- .github/ISSUE_TEMPLATE/generic_issue_ark.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/generic_issue_ark.md diff --git a/.github/ISSUE_TEMPLATE/generic_issue_ark.md b/.github/ISSUE_TEMPLATE/generic_issue_ark.md new file mode 100644 index 00000000000..2eded6c5f05 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/generic_issue_ark.md @@ -0,0 +1,5 @@ +--- +name: File an issue for Ark +about: Create a new issue for the Ark kernel (https://github.com/posit-dev/ark). +labels: ["lang: r", "area: kernels"] +--- From 3f15a0cacd87513af9dacdfd795866278247b520 Mon Sep 17 00:00:00 2001 From: Daniel Falbel Date: Tue, 16 Jul 2024 10:58:43 -0300 Subject: [PATCH 03/38] Adds an integration test for the connections pane. (#3882) Added an integration test for the connections pane. At this level of abstraction, we are not testing individual clicks (like in the smoke tests), but testing the internal logic in the extension that is used by the TreeView API to render the values. --------- Signed-off-by: Daniel Falbel Co-authored-by: Jonathan --- .vscode-test.js | 5 + .../positron-connections/src/connection.ts | 8 ++ .../positron-connections/src/extension.ts | 4 + .../positron-connections/src/test/README.md | 5 + .../src/test/connection.test.ts | 97 +++++++++++++++++++ 5 files changed, 119 insertions(+) create mode 100644 extensions/positron-connections/src/test/README.md create mode 100644 extensions/positron-connections/src/test/connection.test.ts diff --git a/.vscode-test.js b/.vscode-test.js index 1127ca11b84..0f623d16dd7 100644 --- a/.vscode-test.js +++ b/.vscode-test.js @@ -56,6 +56,11 @@ const extensions = [ label: 'positron-r', workspaceFolder: path.join(os.tmpdir(), `positron-r-${Math.floor(Math.random() * 100000)}`), mocha: { timeout: 60_000 } + }, + { + label: 'positron-connections', + workspaceFolder: path.join(os.tmpdir(), `positron-connections-${Math.floor(Math.random() * 100000)}`), + mocha: { timeout: 60_000 } } // --- End Positron --- ]; diff --git a/extensions/positron-connections/src/connection.ts b/extensions/positron-connections/src/connection.ts index 4363425eb63..cc412236f44 100644 --- a/extensions/positron-connections/src/connection.ts +++ b/extensions/positron-connections/src/connection.ts @@ -552,6 +552,14 @@ export class ConnectionItemsProvider this._onDidChangeTreeData.fire(undefined); this.treeItemDecorationProvider.updateFileDecorations([]); } + + /** + * List all connections + * Currently only used for testing purposes. + */ + listConnections() { + return this._connections; + } } /** diff --git a/extensions/positron-connections/src/extension.ts b/extensions/positron-connections/src/extension.ts index 6fd411dc9da..62d7f595919 100644 --- a/extensions/positron-connections/src/extension.ts +++ b/extensions/positron-connections/src/extension.ts @@ -101,4 +101,8 @@ export function activate(context: vscode.ExtensionContext) { () => { connectionProvider.expandConnectionNodes(connectionTreeView); })); + + // this allows vscode.extensions.getExtension('vscode.positron-connections').exports + // to acccess the ConnectionItemsProvider instance + return connectionProvider; } diff --git a/extensions/positron-connections/src/test/README.md b/extensions/positron-connections/src/test/README.md new file mode 100644 index 00000000000..dfdeb2848eb --- /dev/null +++ b/extensions/positron-connections/src/test/README.md @@ -0,0 +1,5 @@ +Launch tests by running this from the repository root: + +```sh +yarn test-extension -l positron-connections +``` diff --git a/extensions/positron-connections/src/test/connection.test.ts b/extensions/positron-connections/src/test/connection.test.ts new file mode 100644 index 00000000000..0bd53175541 --- /dev/null +++ b/extensions/positron-connections/src/test/connection.test.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import { ConnectionItemsProvider } from '../connection'; +import * as mocha from 'mocha'; +import { randomUUID } from 'crypto'; + +suite('Connections pane works for R', () => { + suiteTeardown(() => { + vscode.window.showInformationMessage('All tests done!'); + }); + + test('Can list tables and fields from R connections', async () => { + + // Waits until positron is ready to start a runtime + const info = await assert_or_timeout(async () => { + return await positron.runtime.getPreferredRuntime('r'); + }); + + const session = await positron.runtime.startLanguageRuntime(info!.runtimeId, 'Test connections pane!'); + executeRCode( + session, + 'con <- connections::connection_open(RSQLite::SQLite(), tempfile())', + ); + + const ext = vscode.extensions.getExtension('vscode.positron-connections'); + const provider = ext?.exports; + assert(provider !== undefined); + + // There's some delay between the connection being registered in R, the comm opened with + // positron and the extension being able to get the message. We wait up to 1 second for + // this to happen. + const sqlite = await assert_or_timeout(() => { + const connections = provider.listConnections(); + assert(connections !== undefined); + assert(connections[0].name === "SQLiteConnection"); + return connections[0]; + }); + + // Add a table to the connection + executeRCode( + session, + 'DBI::dbWriteTable(con, "mtcars", mtcars)' + ); + + const catalog = await provider.getChildren(sqlite); + assert(catalog.length === 1); + + const schema = await provider.getChildren(catalog[0]); + assert(schema.length === 1); + + const tables = await provider.getChildren(schema[0]); + assert(tables.length === 1); + const mtcars = tables[0]; + assert(mtcars.name === "mtcars"); + + const fields = await provider.getChildren(mtcars); + assert(fields.length === 11); + assert.notStrictEqual( + fields.map(f => f.name), + ['mpg', 'cyl', 'disp', 'hp', 'drat', 'wt', 'qsec', 'vs', 'am', 'gear', 'carb'] + ); + }); +}); + +async function sleep(time: number) { + return new Promise((resolve) => setTimeout(resolve, time)); +} + +async function assert_or_timeout(fn: () => T, timeout: number = 5000): Promise { + const start = Date.now(); + let error; + while (Date.now() - start < timeout) { + try { + return await fn(); + } catch (err) { + error = err; + await sleep(50); + } + } + // We throw the last error that happened before the timeout + throw error; +} + +function executeRCode(session: positron.LanguageRuntimeSession, code: string) { + session.execute( + code, + randomUUID(), + positron.RuntimeCodeExecutionMode.Interactive, + positron.RuntimeErrorBehavior.Stop + ); +} From 3322d758a8768d7370fb92ef1fc224dc1b57c804 Mon Sep 17 00:00:00 2001 From: Daniel Falbel Date: Tue, 16 Jul 2024 11:21:03 -0300 Subject: [PATCH 04/38] Bump ark to v0.1.117 (#4031) For - https://github.com/posit-dev/ark/pull/432 --- extensions/positron-r/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/positron-r/package.json b/extensions/positron-r/package.json index e057e9b924d..7130fb3306f 100644 --- a/extensions/positron-r/package.json +++ b/extensions/positron-r/package.json @@ -606,7 +606,7 @@ }, "positron": { "binaryDependencies": { - "ark": "0.1.116" + "ark": "0.1.117" }, "minimumRVersion": "4.2.0", "minimumRenvVersion": "1.0.7" From bc73f2f09a2cae189c079a3f5f01334ebc3ee50c Mon Sep 17 00:00:00 2001 From: Brian Lambert Date: Tue, 16 Jul 2024 13:14:21 -0700 Subject: [PATCH 05/38] This PR adds a Data Explorer smoke test that loads a 100 column x 100 row data frame from a Parquet file and verifies that three rows in the file (the first, the middle, and the last) render correctly in Pandas, Polars, and R. (#3947) This PR adds a Data Explorer smoke test that loads a 100 column x 100 row data frame from a Parquet file and verifies that three rows in the file (the first, the middle, and the last) render correctly in Pandas, Polars, and R. The data for this smoke test is loaded from the `qa-example-content` repo. --- .../components/dataGridRowCell.tsx | 2 +- .../browser/positronDataExplorerActions.ts | 85 +++++- .../positronDataExplorerInstance.ts | 5 + .../browser/positronDataExplorerInstance.ts | 11 +- .../browser/tableDataDataGridInstance.tsx | 7 +- .../common/dataExplorerCache.ts | 72 ++++- .../src/positron/positronDataExplorer.ts | 28 ++ .../data-explorer-100x100.test.ts | 268 ++++++++++++++++++ test/smoke/src/main.ts | 2 + 9 files changed, 468 insertions(+), 12 deletions(-) create mode 100644 test/smoke/src/areas/positron/dataexplorer/data-explorer-100x100.test.ts diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridRowCell.tsx b/src/vs/workbench/browser/positronDataGrid/components/dataGridRowCell.tsx index d2faf6be6d0..0d81b433686 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridRowCell.tsx +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridRowCell.tsx @@ -162,7 +162,7 @@ export const DataGridRowCell = (props: DataGridRowCellProps) => { } } -
+
{context.instance.cell(props.columnIndex, props.rowIndex)}
{context.instance.columnResize && diff --git a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts index 7cb4a9feea3..6de57c822d0 100644 --- a/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts +++ b/src/vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.ts @@ -10,6 +10,7 @@ import { ILocalizedString } from 'vs/platform/action/common/action'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { PositronDataExplorerFocused } from 'vs/workbench/common/contextkeys'; +import { IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -39,6 +40,7 @@ const category: ILocalizedString = { */ export const enum PositronDataExplorerCommandId { CopyAction = 'workbench.action.positronDataExplorer.copy', + CopyTableDataAction = 'workbench.action.positronDataExplorer.copyTableData', } /** @@ -113,7 +115,7 @@ class PositronDataExplorerCopyAction extends Action2 { // Notify the user. notificationService.notify({ severity: Severity.Error, - message: localize('positron.dataExplorer.noActiveEditor', "Cannot copy. A Positron Data Explorer is not active."), + message: localize('positron.dataExplorer.copy.noActiveEditor', "Cannot copy. A Positron Data Explorer is not active."), sticky: false }); }; @@ -144,14 +146,93 @@ class PositronDataExplorerCopyAction extends Action2 { return; } - // Copy to the clipboard. + // Copy the selection or cursor cell to the clipboard. await positronDataExplorerInstance.copyToClipboard(); } } +/** + * PositronDataExplorerCopyTableDataAction action. + */ +class PositronDataExplorerCopyTableDataAction extends Action2 { + constructor() { + super({ + id: PositronDataExplorerCommandId.CopyTableDataAction, + title: { + value: localize('positronDataExplorer.copyTableData', 'Copy Table Data'), + original: 'Copy Table Data' + }, + category, + f1: true, + precondition: ContextKeyExpr.and( + POSITRON_DATA_EXPLORER_IS_ACTIVE_EDITOR, + IsDevelopmentContext + ) + }); + } + + /** + * Runs the action. + * @param accessor The services accessor. + */ + async run(accessor: ServicesAccessor): Promise { + // Access the services we need. + const editorService = accessor.get(IEditorService); + const notificationService = accessor.get(INotificationService); + const positronDataExplorerService = accessor.get(IPositronDataExplorerService); + + // Get the Positron data explorer editor. + const positronDataExplorerEditor = getPositronDataExplorerEditorFromEditorPane( + editorService.activeEditorPane + ); + + /** + * Notifies the user that copy operation failed. + */ + const notifyUserThatCopyFailed = () => { + // Notify the user. + notificationService.notify({ + severity: Severity.Error, + message: localize('positron.dataExplorer.copyTableData.noActiveEditor', "Cannot copy table data. A Positron Data Explorer is not active."), + sticky: false + }); + }; + + // Make sure that the Positron data explorer editor was returned. + if (!positronDataExplorerEditor) { + notifyUserThatCopyFailed(); + return; + } + + // Get the identifier. + const identifier = positronDataExplorerEditor.identifier; + + // Make sure the identifier was returned. + if (!identifier) { + notifyUserThatCopyFailed(); + return; + } + + // Get the Positron data explorer instance. + const positronDataExplorerInstance = positronDataExplorerService.getInstance( + identifier + ); + + // Make sure the Positron data explorer instance was returned. + if (!positronDataExplorerInstance) { + notifyUserThatCopyFailed(); + return; + } + + // Copy the table data to the clipboard. + await positronDataExplorerInstance.copyTableDataToClipboard(); + } +} + /** * Registers Positron data explorer actions. */ export function registerPositronDataExplorerActions() { registerAction2(PositronDataExplorerCopyAction); + registerAction2(PositronDataExplorerCopyTableDataAction); } diff --git a/src/vs/workbench/services/positronDataExplorer/browser/interfaces/positronDataExplorerInstance.ts b/src/vs/workbench/services/positronDataExplorer/browser/interfaces/positronDataExplorerInstance.ts index 47762e0ac8e..fee4c949951 100644 --- a/src/vs/workbench/services/positronDataExplorer/browser/interfaces/positronDataExplorerInstance.ts +++ b/src/vs/workbench/services/positronDataExplorer/browser/interfaces/positronDataExplorerInstance.ts @@ -73,4 +73,9 @@ export interface IPositronDataExplorerInstance extends IDisposable { * Copies the selection or cursor cell to the clipboard. */ copyToClipboard(): Promise; + + /** + * Copies the table data to the clipboard. + */ + copyTableDataToClipboard(): Promise; } diff --git a/src/vs/workbench/services/positronDataExplorer/browser/positronDataExplorerInstance.ts b/src/vs/workbench/services/positronDataExplorer/browser/positronDataExplorerInstance.ts index 86e84321f70..6bb59a50ebf 100644 --- a/src/vs/workbench/services/positronDataExplorer/browser/positronDataExplorerInstance.ts +++ b/src/vs/workbench/services/positronDataExplorer/browser/positronDataExplorerInstance.ts @@ -125,11 +125,11 @@ export class PositronDataExplorerInstance extends Disposable implements IPositro ); this._tableDataDataGridInstance = new TableDataDataGridInstance( this._commandService, + this._configurationService, this._keybindingService, this._layoutService, this._dataExplorerClientInstance, - this._dataExplorerCache, - this._configurationService + this._dataExplorerCache ); // Add the onDidClose event handler. @@ -298,6 +298,13 @@ export class PositronDataExplorerInstance extends Disposable implements IPositro this._clipboardService.writeText(text); } + /** + * Copies the table data to the clipboard. + */ + async copyTableDataToClipboard(): Promise { + this._clipboardService.writeText(await this._dataExplorerCache.getTableData()); + } + /** * onDidClose event. */ diff --git a/src/vs/workbench/services/positronDataExplorer/browser/tableDataDataGridInstance.tsx b/src/vs/workbench/services/positronDataExplorer/browser/tableDataDataGridInstance.tsx index 5ec6b238202..93f6836625d 100644 --- a/src/vs/workbench/services/positronDataExplorer/browser/tableDataDataGridInstance.tsx +++ b/src/vs/workbench/services/positronDataExplorer/browser/tableDataDataGridInstance.tsx @@ -12,6 +12,7 @@ import { Emitter } from 'vs/base/common/event'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IColumnSortKey } from 'vs/workbench/browser/positronDataGrid/interfaces/columnSortKey'; import { DataExplorerCache } from 'vs/workbench/services/positronDataExplorer/common/dataExplorerCache'; import { TableDataCell } from 'vs/workbench/services/positronDataExplorer/browser/components/tableDataCell'; @@ -23,10 +24,9 @@ import { DataExplorerClientInstance } from 'vs/workbench/services/languageRuntim import { CustomContextMenuSeparator } from 'vs/workbench/browser/positronComponents/customContextMenu/customContextMenuSeparator'; import { PositronDataExplorerCommandId } from 'vs/workbench/contrib/positronDataExplorerEditor/browser/positronDataExplorerActions'; import { CustomContextMenuEntry, showCustomContextMenu } from 'vs/workbench/browser/positronComponents/customContextMenu/customContextMenu'; +import { dataExplorerExperimentalFeatureEnabled } from 'vs/workbench/services/positronDataExplorer/common/positronDataExplorerExperimentalConfig'; import { BackendState, ColumnSchema, DataSelection, DataSelectionCellRange, DataSelectionIndices, DataSelectionKind, DataSelectionRange, DataSelectionSingleCell, ExportFormat, RowFilter, SupportStatus } from 'vs/workbench/services/languageRuntime/common/positronDataExplorerComm'; import { ClipboardCell, ClipboardCellRange, ClipboardColumnIndexes, ClipboardColumnRange, ClipboardData, ClipboardRowIndexes, ClipboardRowRange, ColumnSelectionState, ColumnSortKeyDescriptor, DataGridInstance, RowSelectionState } from 'vs/workbench/browser/positronDataGrid/classes/dataGridInstance'; -import { dataExplorerExperimentalFeatureEnabled } from 'vs/workbench/services/positronDataExplorer/common/positronDataExplorerExperimentalConfig'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; /** * Localized strings. @@ -51,6 +51,7 @@ export class TableDataDataGridInstance extends DataGridInstance { /** * Constructor. * @param _commandService The command service. + * @param _configurationService The configuration service. * @param _keybindingService The keybinding service. * @param _layoutService The layout service. * @param _dataExplorerClientInstance The DataExplorerClientInstance. @@ -58,11 +59,11 @@ export class TableDataDataGridInstance extends DataGridInstance { */ constructor( private readonly _commandService: ICommandService, + private readonly _configurationService: IConfigurationService, private readonly _keybindingService: IKeybindingService, private readonly _layoutService: ILayoutService, private readonly _dataExplorerClientInstance: DataExplorerClientInstance, private readonly _dataExplorerCache: DataExplorerCache, - private readonly _configurationService: IConfigurationService, ) { // Call the base class's constructor. super({ diff --git a/src/vs/workbench/services/positronDataExplorer/common/dataExplorerCache.ts b/src/vs/workbench/services/positronDataExplorer/common/dataExplorerCache.ts index 2c48e64ce52..c93f1744165 100644 --- a/src/vs/workbench/services/positronDataExplorer/common/dataExplorerCache.ts +++ b/src/vs/workbench/services/positronDataExplorer/common/dataExplorerCache.ts @@ -6,12 +6,13 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { DataExplorerClientInstance } from 'vs/workbench/services/languageRuntime/common/languageRuntimeDataExplorerClient'; -import { ColumnProfileType, ColumnSchema, ColumnSummaryStats, TableData } from 'vs/workbench/services/languageRuntime/common/positronDataExplorerComm'; +import { ColumnProfileType, ColumnSchema, ColumnSummaryStats } from 'vs/workbench/services/languageRuntime/common/positronDataExplorerComm'; /** * Constants. */ const OVERSCAN_FACTOR = 3; +const CHUNK_SIZE = 100; /** * Creates an array from an index range. @@ -47,8 +48,6 @@ export enum DataCellKind { UNKNOWN = 'unknown' } - - /** * DataCell interface */ @@ -299,6 +298,71 @@ export class DataExplorerCache extends Disposable { return this._dataCellCache.get(`${columnIndex},${rowIndex}`); } + /** + * Gets the table data. + * @returns The table data as a TSV string. + */ + async getTableData(): Promise { + // The cell values. + const cellValues = new Map(); + + // Loop over chunks of columns. + for (let columnIndex = 0; columnIndex < this._columns; columnIndex += CHUNK_SIZE) { + // Loop over chunks of rows. + for (let rowIndex = 0; rowIndex < this._rows; rowIndex += CHUNK_SIZE) { + // Get the table data. + const maxColumnIndex = Math.min(columnIndex + CHUNK_SIZE, this._columns); + const maxRowIndex = Math.min(rowIndex + CHUNK_SIZE, this._rows); + const tableData = await this._dataExplorerClientInstance.getDataValues( + rowIndex, + maxRowIndex, + arrayFromIndexRange(columnIndex, maxColumnIndex) + ); + + // Process the table data into cell values. + for (let ci = 0; ci < maxColumnIndex - columnIndex; ci++) { + for (let ri = 0; ri < maxRowIndex - rowIndex; ri++) { + // Get the cell value. + const cellValue = tableData.columns[ci][ri]; + + // Add the cell. + if (typeof cellValue === 'number') { + cellValues.set( + `${rowIndex + ri},${columnIndex + ci}`, + decodeSpecialValue(cellValue).formatted + ); + } else { + cellValues.set(`${rowIndex + ri},${columnIndex + ci}`, cellValue); + } + } + } + } + } + + // Build the result. + let result = ''; + for (let rowIndex = 0; rowIndex < this._rows; rowIndex++) { + // Append the newline before writing the row to the result. + if (rowIndex) { + result += '\n'; + } + + // Write the row to the result. + for (let columnIndex = 0; columnIndex < this._columns; columnIndex++) { + // Append the tab separator before writing the cell value. + if (columnIndex) { + result += '\t'; + } + + // Write the cell value to the row. + result += cellValues.get(`${rowIndex},${columnIndex}`); + } + } + + // Done. + return result; + } + //#endregion Public Methods //#region Private Methods @@ -426,7 +490,7 @@ export class DataExplorerCache extends Disposable { const rows = rowIndices[rowIndices.length - 1] - rowIndices[0] + 1; // Get the data values. - const tableData: TableData = await this._dataExplorerClientInstance.getDataValues( + const tableData = await this._dataExplorerClientInstance.getDataValues( rowIndices[0], rows, columnIndices diff --git a/test/automation/src/positron/positronDataExplorer.ts b/test/automation/src/positron/positronDataExplorer.ts index 7520b0d9aca..a84f2b02fe5 100644 --- a/test/automation/src/positron/positronDataExplorer.ts +++ b/test/automation/src/positron/positronDataExplorer.ts @@ -133,4 +133,32 @@ export class PositronDataExplorer { await this.code.driver.getLocator(`.positron-modal-overlay div.title:has-text("${menuItem}")`).click(); } + + async home(): Promise { + await this.code.dispatchKeybinding('home'); + } + + async cmdCtrlHome(): Promise { + if (process.platform === 'darwin') { + await this.code.dispatchKeybinding('cmd+home'); + } else { + await this.code.dispatchKeybinding('ctrl+home'); + } + } + + async arrowDown(): Promise { + await this.code.dispatchKeybinding('ArrowDown'); + } + + async arrowRight(): Promise { + await this.code.dispatchKeybinding('ArrowRight'); + } + + async arrowUp(): Promise { + await this.code.dispatchKeybinding('ArrowUp'); + } + + async arrowLeft(): Promise { + await this.code.dispatchKeybinding('ArrowLeft'); + } } diff --git a/test/smoke/src/areas/positron/dataexplorer/data-explorer-100x100.test.ts b/test/smoke/src/areas/positron/dataexplorer/data-explorer-100x100.test.ts new file mode 100644 index 00000000000..c2b2d8c53c1 --- /dev/null +++ b/test/smoke/src/areas/positron/dataexplorer/data-explorer-100x100.test.ts @@ -0,0 +1,268 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import { join } from 'path'; +import { expect } from '@playwright/test'; +import { installAllHandlers } from '../../../utils'; +import { Application, Logger, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation'; + +/** + * Sets up the Data Explorer 100x100 smoke test. + * @param logger The logger. + */ +export function setupDataExplorer100x100Test(logger: Logger) { + /** + * Data Explorer 100x100. + */ + describe('Data Explorer 100x100', function () { + // Shared before/after handling. + installAllHandlers(logger); + + + /** + * Tests the data explorer. + * @param app The application. + * @param language The language. + * @param prompt The prompt. + * @param commands Commands to run to set up the test. + * @param dataFrameName The data frame name. + * @param tsvFilePath The TSV file path. + */ + const testDataExplorer = async ( + app: Application, + language: 'Python' | 'R', + prompt: string, + commands: string[], + dataFrameName: string, + tsvFilePath: string + ): Promise => { + // Execute commands. + for (let i = 0; i < commands.length; i++) { + await app.workbench.positronConsole.executeCode( + language, + commands[i], + prompt + ); + } + + // Open the data frame. + await expect(async () => { + await app.workbench.positronVariables.doubleClickVariableRow(dataFrameName); + await app.code.driver.getLocator(`.label-name:has-text("Data: ${dataFrameName}")`).innerText(); + }).toPass(); + + // Drive focus into the data explorer. + await app.workbench.positronDataExplorer.clickUpperLeftCorner(); + + // Load the TSV file that is used to verify the data and split it into lines. + const tsvFile = fs.readFileSync(tsvFilePath, { encoding: 'utf8' }); + let lines: string[]; + if (process.platform === 'win32') { + lines = tsvFile.split('\r\n'); + } else { + lines = tsvFile.split('\n'); + } + + // Get the TSV values. + const tsvValues: string[][] = []; + for (let rowIndex = 0; rowIndex < lines.length; rowIndex++) { + tsvValues.push(lines[rowIndex].split('\t')); + } + + /** + * Tests the row at the specified row index. + * @param rowIndex The row index of the row under test. + */ + const testRow = async (rowIndex: number) => { + // Scroll to home and put the cursor there. + await app.workbench.positronDataExplorer.cmdCtrlHome(); + + // Navigate to the row under test. + for (let i = 0; i < rowIndex; i++) { + await app.workbench.positronDataExplorer.arrowDown(); + } + + // Test each cell in the row under test. + const row = tsvValues[rowIndex]; + for (let columnIndex = 0; columnIndex < row.length; columnIndex++) { + // Get the cell. + const cell = await app.code.waitForElement(`#data-grid-row-cell-content-${columnIndex}-${rowIndex} .text-container .text-value`); + + // Test the cell. + expect(cell.textContent).toStrictEqual(row[columnIndex]); + + // Move to the next cell. + await app.workbench.positronDataExplorer.arrowRight(); + } + }; + + // Check the first row, the middle row, and the last row. + await testRow(0); + await testRow(Math.trunc(tsvValues.length / 2)); + await testRow(tsvValues.length - 1); + }; + + /** + * Constructs the Parquet file path. + * @param app The application. + * @returns The Parquet file path. + */ + const parquetFilePath = (app: Application) => { + // Set the path to the Parquet file. + let parquetFilePath = join( + app.workspacePathOrFolder, + 'data-files', + '100x100', + '100x100.parquet' + ); + + // On Windows, double escape the path. + if (process.platform === 'win32') { + parquetFilePath = parquetFilePath.replaceAll('\\', '\\\\'); + } + + // Return the path to the Parquet file. + return parquetFilePath; + }; + + /** + * Data Explorer 100x100 - Python - Pandas. + */ + describe('Data Explorer 100x100 - Python - Pandas', function () { + /** + * Before hook. + */ + before(async function () { + const app = this.app as Application; + const pythonFixtures = new PositronPythonFixtures(app); + await pythonFixtures.startPythonInterpreter(); + }); + + /** + * After hook. + */ + after(async function () { + const app = this.app as Application; + await app.workbench.positronDataExplorer.closeDataExplorer(); + }); + + /** + * Data Explorer 100x100 - Python - Pandas - Smoke Test. + */ + it('Data Explorer 100x100 - Python - Pandas - Smoke Test', async function () { + // Get the app. + const app = this.app as Application; + + // Test the data explorer. + const dataFrameName = 'pandas100x100'; + await testDataExplorer( + app, + 'Python', + '>>>', + [ + 'import pandas as pd', + `${dataFrameName} = pd.read_parquet("${parquetFilePath(app)}")`, + ], + dataFrameName, + join(app.workspacePathOrFolder, 'data-files', '100x100', 'pandas-100x100.tsv') + ); + }); + }); + + /** + * Data Explorer 100x100 - Python - Polars. + */ + describe('Data Explorer 100x100 - Python - Polars', function () { + /** + * Before hook. + */ + before(async function () { + const app = this.app as Application; + const pythonFixtures = new PositronPythonFixtures(app); + await pythonFixtures.startPythonInterpreter(); + }); + + /** + * After hook. + */ + after(async function () { + const app = this.app as Application; + await app.workbench.positronDataExplorer.closeDataExplorer(); + }); + + /** + * Data Explorer 100x100 - Python - Polars - Smoke Test. + */ + it('Data Explorer 100x100 - Python - Polars - Smoke Test', async function () { + // Get the app. + const app = this.app as Application; + + // Test the data explorer. + const dataFrameName = 'polars100x100'; + await testDataExplorer( + app, + 'Python', + '>>>', + [ + 'import polars', + `${dataFrameName} = polars.read_parquet("${parquetFilePath(app)}")`, + ], + dataFrameName, + join(app.workspacePathOrFolder, 'data-files', '100x100', 'polars-100x100.tsv') + ); + }); + }); + + /** + * Data Explorer 100x100 - R. + */ + describe('Data Explorer 100x100 - R', function () { + /** + * Before hook. + */ + before(async function () { + const app = this.app as Application; + const rFixtures = new PositronRFixtures(app); + await rFixtures.startRInterpreter(); + }); + + /** + * After hook. + */ + after(async function () { + const app = this.app as Application; + await app.workbench.positronDataExplorer.closeDataExplorer(); + }); + + /** + * Data Explorer 100x100 - R - Smoke Test. + */ + it('Data Explorer 100x100 - R - Smoke Test', async function () { + // Get the app. + const app = this.app as Application; + + // Test the data explorer. + const dataFrameName = 'r100x100'; + await testDataExplorer( + app, + 'R', + '>', + [ + 'library(arrow)', + `${dataFrameName} <- read_parquet("${parquetFilePath(app)}")`, + ], + dataFrameName, + join( + app.workspacePathOrFolder, + 'data-files', + '100x100', + process.platform === 'linux' ? 'r-100x100-linux.tsv' : 'r-100x100.tsv' + ) + ); + }); + }); + }); +} diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index a214d270c37..0b2d99e33cb 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -31,6 +31,7 @@ import { retry, timeout } from './utils'; // import { setup as setupTerminalTests } from './areas/terminal/terminal.test'; // import { setup as setupTaskTests } from './areas/task/task.test'; import { setup as setupVariablesTest } from './areas/positron/variables/variablespane.test'; +import { setupDataExplorer100x100Test } from './areas/positron/dataexplorer/data-explorer-100x100.test'; import { setup as setupDataExplorerTest } from './areas/positron/dataexplorer/dataexplorer.test'; import { setup as setupPlotsTest } from './areas/positron/plots/plots.test'; import { setup as setupPythonConsoleTest } from './areas/positron/console/python-console.test'; @@ -432,6 +433,7 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { // if (!opts.web && !opts.remote) { setupLaunchTests(logger); } setupVariablesTest(logger); setupDataExplorerTest(logger); + setupDataExplorer100x100Test(logger); setupPlotsTest(logger); setupPythonConsoleTest(logger); setupRConsoleTest(logger); From e0bc2f86e46b6a494cf63a588a0ffd4884ad3c03 Mon Sep 17 00:00:00 2001 From: "Jennifer (Jenny) Bryan" Date: Tue, 16 Jul 2024 13:51:48 -0700 Subject: [PATCH 06/38] Update startup log (#3897) --- src/vs/code/electron-main/app.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 0f9cd91873b..bf97eda3640 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -573,7 +573,9 @@ export class CodeApplication extends Disposable { } async startup(): Promise { - this.logService.debug('Starting VS Code'); + // --- Start Positron --- + this.logService.debug('Starting Positron'); + // --- End Positron --- this.logService.debug(`from: ${this.environmentMainService.appRoot}`); this.logService.debug('args:', this.environmentMainService.args); From 02fb7670b8c70e2c9bdddfd7c371ee0e821e663d Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 16 Jul 2024 14:36:42 -0700 Subject: [PATCH 07/38] Upstream VSCode changes from 1.90.0 to 1.91.0 (#4034) Merges upstream VS Code 1.91 into Positron. Co-authored-by: positron-bot[bot] <173392469+positron-bot[bot]@users.noreply.github.com> --- .configurations/configuration.dsc.yaml | 4 +- .devcontainer/Dockerfile | 2 +- .eslintrc.json | 2 +- .github/classifier.json | 4 +- .github/workflows/locker.yml | 3 +- .github/workflows/on-open.yml | 11 + .github/workflows/on-reopen.yml | 22 + .../src/coverageProvider.ts | 74 +- .../src/extension.ts | 13 +- .../src/testOutputScanner.ts | 94 +- .../src/vscodeTestRunner.ts | 4 +- .../tsconfig.json | 2 +- .vscode/notebooks/api.github-issues | 2 +- .vscode/notebooks/endgame.github-issues | 2 +- .vscode/notebooks/my-endgame.github-issues | 4 +- .vscode/notebooks/my-work.github-issues | 4 +- .vscode/settings.json | 3 +- .yarnrc | 2 +- ThirdPartyNotices.txt | 4 +- build/.webignore | 3 + .../product-build-linux-legacy-server.yml | 30 +- .../linux/product-build-linux.yml | 42 +- .../linux/verify-glibc-requirements.sh | 12 +- build/azure-pipelines/product-build.yml | 18 +- build/azure-pipelines/product-compile.yml | 11 +- build/azure-pipelines/sdl-scan.yml | 433 ++--- .../azure-pipelines/web/product-build-web.yml | 11 +- .../win32/product-build-win32-test.yml | 10 + build/gulpfile.extensions.js | 2 +- build/gulpfile.reh.js | 5 +- build/gulpfile.vscode.js | 2 +- build/gulpfile.vscode.linux.js | 46 +- build/gulpfile.vscode.web.js | 2 +- build/lib/bundle.js | 3 + build/lib/bundle.ts | 3 + build/lib/compilation.js | 33 +- build/lib/compilation.ts | 36 +- build/lib/electron.js | 2 +- build/lib/electron.ts | 2 +- build/lib/extensions.js | 2 +- build/lib/extensions.ts | 2 +- build/lib/layersChecker.js | 55 +- build/lib/layersChecker.ts | 61 +- .../lib/stylelint/vscode-known-variables.json | 32 +- build/lib/tsb/transpiler.js | 4 +- build/lib/tsb/transpiler.ts | 4 +- build/package.json | 3 +- build/secrets/.secrets.baseline | 49 +- build/yarn.lock | 201 +-- cli/ThirdPartyNotices.txt | 175 +- cli/src/async_pipe.rs | 2 +- cli/src/auth.rs | 2 +- cli/src/commands/args.rs | 4 +- cli/src/commands/serve_web.rs | 55 +- cli/src/constants.rs | 2 +- cli/src/tunnels/control_server.rs | 9 +- cli/src/tunnels/dev_tunnels.rs | 10 +- cli/src/tunnels/local_forwarding.rs | 62 +- cli/src/tunnels/port_forwarder.rs | 9 +- cli/src/tunnels/protocol.rs | 33 +- extensions/configuration-editing/package.json | 4 +- extensions/cpp/package.json | 4 + .../client/src/browser/cssClientMain.ts | 9 +- .../client/tsconfig.json | 5 +- extensions/css-language-features/package.json | 2 +- .../css-language-features/server/package.json | 4 +- .../css-language-features/server/yarn.lock | 46 +- extensions/css-language-features/yarn.lock | 38 +- .../extension-editing/src/extensionLinter.ts | 3 +- extensions/git/package.json | 10 +- extensions/git/src/api/api1.ts | 1 + extensions/git/src/api/git.d.ts | 2 + extensions/git/src/commands.ts | 106 +- extensions/git/src/decorationProvider.ts | 6 +- extensions/git/src/encoding.ts | 27 +- extensions/git/src/git.ts | 21 +- extensions/git/src/historyProvider.ts | 122 +- extensions/git/src/model.ts | 15 +- extensions/git/src/repository.ts | 9 +- extensions/git/yarn.lock | 8 +- extensions/go/cgmanifest.json | 4 +- extensions/go/syntaxes/go.tmLanguage.json | 62 +- .../client/src/browser/htmlClientMain.ts | 9 +- .../client/tsconfig.json | 5 +- .../html-language-features/package.json | 2 +- .../server/package.json | 6 +- .../html-language-features/server/yarn.lock | 54 +- extensions/html-language-features/yarn.lock | 38 +- extensions/ipynb/package.json | 20 +- extensions/ipynb/package.nls.json | 1 + extensions/ipynb/src/ipynbMain.ts | 7 - .../client/src/browser/jsonClientMain.ts | 8 +- .../client/tsconfig.json | 5 +- .../json-language-features/package.json | 2 +- .../server/package.json | 4 +- .../json-language-features/server/yarn.lock | 53 +- extensions/json-language-features/yarn.lock | 38 +- extensions/latex/cgmanifest.json | 4 +- extensions/latex/syntaxes/TeX.tmLanguage.json | 22 +- .../language-configuration.json | 4 + .../media/markdown.css | 2 +- .../microsoft-authentication/package.json | 3 +- .../microsoft-authentication/src/AADHelper.ts | 85 +- .../microsoft-authentication/src/extension.ts | 6 +- .../microsoft-authentication/tsconfig.json | 3 +- extensions/notebook-renderers/src/index.ts | 6 +- .../notebook-renderers/src/textHelper.ts | 10 + extensions/notebook-renderers/yarn.lock | 6 +- extensions/npm/yarn.lock | 16 +- extensions/package.json | 2 +- extensions/python/cgmanifest.json | 2 +- extensions/tunnel-forwarding/src/extension.ts | 6 +- .../typescript-language-features/package.json | 10 +- .../package.nls.json | 3 +- .../src/configuration/configuration.ts | 6 + .../src/languageFeatures/copyPaste.ts | 17 +- .../src/languageFeatures/diagnostics.ts | 31 +- .../fileConfigurationManager.ts | 1 - .../src/languageFeatures/refactor.ts | 14 +- .../src/languageProvider.ts | 8 +- .../src/tsServer/api.ts | 1 + .../src/tsServer/bufferSyncSupport.ts | 46 +- .../src/tsServer/protocol/protocol.const.ts | 1 + .../src/tsServer/protocol/protocol.d.ts | 65 - .../src/tsServer/serverProcess.electron.ts | 3 +- .../src/tsServer/spawner.ts | 2 +- .../src/typeConverters.ts | 14 +- .../src/typeScriptServiceClientHost.ts | 10 +- .../src/typescriptServiceClient.ts | 9 +- extensions/vscode-api-tests/package.json | 23 +- .../src/singlefolder-tests/lm.test.ts | 153 ++ .../src/singlefolder-tests/proxy.test.ts | 77 + extensions/vscode-api-tests/yarn.lock | 19 + extensions/yarn.lock | 24 +- package.json | 27 +- product.json | 13 +- remote/package.json | 20 +- remote/web/package.json | 15 +- remote/web/yarn.lock | 78 +- remote/yarn.lock | 114 +- resources/linux/snap/snapcraft.yaml | 3 + scripts/code.bat | 9 +- scripts/code.sh | 7 +- scripts/xterm-update.js | 1 + src/bootstrap-window.js | 7 +- src/main.js | 29 +- src/server-main.js | 7 +- src/tsconfig.json | 11 + src/tsconfig.tsec.json | 1 + src/vs/base/browser/dnd.ts | 2 +- src/vs/base/browser/dom.ts | 160 +- src/vs/base/browser/domObservable.ts | 17 + src/vs/base/browser/markdownRenderer.ts | 10 +- src/vs/base/browser/trustedTypes.ts | 3 +- .../browser/ui/actionbar/actionViewItems.ts | 6 +- src/vs/base/browser/ui/actionbar/actionbar.ts | 2 +- src/vs/base/browser/ui/button/button.ts | 8 +- .../browser/ui/centered/centeredViewLayout.ts | 6 +- .../browser/ui/contextview/contextview.ts | 4 +- src/vs/base/browser/ui/dropdown/dropdown.ts | 6 +- .../ui/dropdown/dropdownActionViewItem.ts | 2 +- src/vs/base/browser/ui/grid/gridview.ts | 4 +- .../ui/highlightedlabel/highlightedLabel.ts | 6 +- src/vs/base/browser/ui/hover/hover.ts | 61 +- src/vs/base/browser/ui/hover/hoverDelegate.ts | 4 +- .../base/browser/ui/hover/hoverDelegate2.ts | 4 +- src/vs/base/browser/ui/hover/hoverWidget.css | 5 + src/vs/base/browser/ui/hover/hoverWidget.ts | 6 + src/vs/base/browser/ui/iconLabel/iconLabel.ts | 12 +- .../browser/ui/iconLabel/simpleIconLabel.ts | 6 +- src/vs/base/browser/ui/inputbox/inputBox.ts | 6 +- .../ui/keybindingLabel/keybindingLabel.ts | 6 +- src/vs/base/browser/ui/list/listView.ts | 8 +- src/vs/base/browser/ui/list/rowCache.ts | 10 +- src/vs/base/browser/ui/sash/sash.ts | 2 +- .../browser/ui/selectBox/selectBoxCustom.ts | 23 +- src/vs/base/browser/ui/splitview/paneview.ts | 2 +- src/vs/base/browser/ui/splitview/splitview.ts | 2 +- src/vs/base/browser/ui/table/tableWidget.ts | 2 +- src/vs/base/browser/ui/toggle/toggle.ts | 6 +- src/vs/base/browser/ui/toolbar/toolbar.ts | 16 +- src/vs/base/browser/ui/tree/abstractTree.ts | 2 +- src/vs/base/browser/ui/tree/asyncDataTree.ts | 4 - src/vs/base/common/collections.ts | 58 + src/vs/base/common/equals.ts | 31 +- src/vs/base/common/errors.ts | 13 + src/vs/base/common/event.ts | 33 +- src/vs/base/common/history.ts | 14 +- src/vs/base/common/iterator.ts | 12 +- src/vs/base/common/json.ts | 34 - .../common/{stripComments.d.ts => jsonc.d.ts} | 10 + .../common/{stripComments.js => jsonc.js} | 25 +- src/vs/base/common/numbers.ts | 27 + src/vs/base/common/observable.ts | 3 + src/vs/base/common/observableInternal/api.ts | 31 + .../base/common/observableInternal/autorun.ts | 4 +- src/vs/base/common/observableInternal/base.ts | 21 +- .../common/observableInternal/debugName.ts | 8 +- .../base/common/observableInternal/derived.ts | 33 +- .../observableInternal/lazyObservableValue.ts | 146 ++ .../base/common/observableInternal/logging.ts | 4 +- .../base/common/observableInternal/promise.ts | 8 +- .../base/common/observableInternal/utils.ts | 113 +- src/vs/base/common/performance.js | 5 +- src/vs/base/common/product.ts | 1 + src/vs/base/node/pfs.ts | 81 +- src/vs/base/node/unc.js | 6 +- src/vs/base/parts/ipc/common/ipc.ts | 12 +- src/vs/base/parts/ipc/node/ipc.net.ts | 14 + .../parts/ipc/test/browser/ipc.mp.test.ts | 2 +- src/vs/base/parts/ipc/test/common/ipc.test.ts | 2 +- .../ipc/test/electron-sandbox/ipc.mp.test.ts | 2 +- .../ipc/test/node/ipc.cp.integrationTest.ts | 2 +- .../base/parts/ipc/test/node/ipc.net.test.ts | 27 +- .../sandbox/electron-sandbox/electronTypes.ts | 16 + .../parts/sandbox/electron-sandbox/globals.ts | 3 +- .../parts/sandbox/electron-sandbox/preload.js | 31 +- .../test/electron-sandbox/globals.test.ts | 5 +- src/vs/base/test/browser/actionbar.test.ts | 2 +- src/vs/base/test/browser/browser.test.ts | 2 +- src/vs/base/test/browser/comparers.test.ts | 2 +- src/vs/base/test/browser/dom.test.ts | 2 +- .../browser/formattedTextRenderer.test.ts | 2 +- src/vs/base/test/browser/hash.test.ts | 2 +- .../test/browser/highlightedLabel.test.ts | 2 +- src/vs/base/test/browser/iconLabels.test.ts | 2 +- src/vs/base/test/browser/indexedDB.test.ts | 2 +- .../test/browser/markdownRenderer.test.ts | 11 +- src/vs/base/test/browser/progressBar.test.ts | 4 +- .../ui/contextview/contextview.test.ts | 2 +- src/vs/base/test/browser/ui/grid/grid.test.ts | 2 +- .../test/browser/ui/grid/gridview.test.ts | 2 +- src/vs/base/test/browser/ui/grid/util.ts | 2 +- .../test/browser/ui/list/listView.test.ts | 2 +- .../test/browser/ui/list/listWidget.test.ts | 2 +- .../test/browser/ui/list/rangeMap.test.ts | 2 +- .../base/test/browser/ui/menu/menubar.test.ts | 2 +- .../ui/scrollbar/scrollableElement.test.ts | 2 +- .../ui/scrollbar/scrollbarState.test.ts | 2 +- .../browser/ui/splitview/splitview.test.ts | 2 +- .../browser/ui/tree/asyncDataTree.test.ts | 2 +- .../ui/tree/compressedObjectTreeModel.test.ts | 2 +- .../test/browser/ui/tree/dataTree.test.ts | 2 +- .../browser/ui/tree/indexTreeModel.test.ts | 2 +- .../test/browser/ui/tree/objectTree.test.ts | 2 +- .../browser/ui/tree/objectTreeModel.test.ts | 2 +- src/vs/base/test/common/arrays.test.ts | 2 +- src/vs/base/test/common/arraysFind.test.ts | 2 +- src/vs/base/test/common/assert.test.ts | 2 +- src/vs/base/test/common/async.test.ts | 2 +- src/vs/base/test/common/buffer.test.ts | 2 +- src/vs/base/test/common/cache.test.ts | 2 +- src/vs/base/test/common/cancellation.test.ts | 2 +- src/vs/base/test/common/charCode.test.ts | 2 +- src/vs/base/test/common/collections.test.ts | 68 +- src/vs/base/test/common/color.test.ts | 2 +- src/vs/base/test/common/console.test.ts | 2 +- src/vs/base/test/common/decorators.test.ts | 2 +- src/vs/base/test/common/diff/diff.test.ts | 2 +- src/vs/base/test/common/errors.test.ts | 2 +- src/vs/base/test/common/event.test.ts | 2 +- src/vs/base/test/common/extpath.test.ts | 2 +- src/vs/base/test/common/filters.test.ts | 2 +- src/vs/base/test/common/fuzzyScorer.test.ts | 2 +- src/vs/base/test/common/glob.test.ts | 2 +- src/vs/base/test/common/history.test.ts | 2 +- src/vs/base/test/common/iconLabels.test.ts | 2 +- src/vs/base/test/common/iterator.test.ts | 2 +- src/vs/base/test/common/json.test.ts | 2 +- src/vs/base/test/common/jsonEdit.test.ts | 2 +- src/vs/base/test/common/jsonFormatter.test.ts | 2 +- ...tripComments.test.ts => jsonParse.test.ts} | 72 +- src/vs/base/test/common/keyCodes.test.ts | 2 +- src/vs/base/test/common/keybindings.test.ts | 2 +- src/vs/base/test/common/labels.test.ts | 2 +- src/vs/base/test/common/lazy.test.ts | 2 +- src/vs/base/test/common/lifecycle.test.ts | 2 +- src/vs/base/test/common/linkedList.test.ts | 2 +- src/vs/base/test/common/linkedText.test.ts | 2 +- src/vs/base/test/common/map.test.ts | 2 +- .../base/test/common/markdownString.test.ts | 2 +- src/vs/base/test/common/marshalling.test.ts | 2 +- src/vs/base/test/common/mime.test.ts | 2 +- src/vs/base/test/common/network.test.ts | 2 +- src/vs/base/test/common/normalization.test.ts | 2 +- src/vs/base/test/common/numbers.test.ts | 27 + src/vs/base/test/common/objects.test.ts | 2 +- src/vs/base/test/common/observable.test.ts | 33 +- src/vs/base/test/common/paging.test.ts | 2 +- src/vs/base/test/common/path.test.ts | 2 +- src/vs/base/test/common/prefixTree.test.ts | 2 +- src/vs/base/test/common/processes.test.ts | 2 +- src/vs/base/test/common/resourceTree.test.ts | 2 +- src/vs/base/test/common/resources.test.ts | 2 +- src/vs/base/test/common/scrollable.test.ts | 2 +- src/vs/base/test/common/skipList.test.ts | 2 +- src/vs/base/test/common/stream.test.ts | 2 +- src/vs/base/test/common/strings.test.ts | 2 +- .../test/common/ternarySearchtree.test.ts | 2 +- src/vs/base/test/common/tfIdf.test.ts | 2 +- src/vs/base/test/common/types.test.ts | 2 +- src/vs/base/test/common/uri.test.ts | 2 +- src/vs/base/test/common/uuid.test.ts | 2 +- src/vs/base/test/node/css.build.test.ts | 2 +- src/vs/base/test/node/extpath.test.ts | 2 +- src/vs/base/test/node/id.test.ts | 2 +- src/vs/base/test/node/nodeStreams.test.ts | 2 +- src/vs/base/test/node/pfs/pfs.test.ts | 2 +- src/vs/base/test/node/port.test.ts | 2 +- src/vs/base/test/node/powershell.test.ts | 2 +- .../processes/processes.integrationTest.ts | 2 +- src/vs/base/test/node/uri.perf.test.ts | 2 +- src/vs/base/test/node/zip/zip.test.ts | 2 +- src/vs/code/electron-main/app.ts | 24 +- .../processExplorer/processExplorer.js | 8 +- .../electron-sandbox/workbench/workbench.js | 23 +- .../node/sharedProcess/sharedProcessMain.ts | 4 +- .../editor/browser/config/charWidthReader.ts | 2 +- .../browser/controller/textAreaHandler.ts | 2 +- src/vs/editor/browser/coreCommands.ts | 7 +- src/vs/editor/browser/observableCodeEditor.ts | 282 ++++ src/vs/editor/browser/observableUtilities.ts | 71 - .../services/abstractCodeEditorService.ts | 2 +- .../services/hoverService/hoverService.ts | 58 +- .../hoverService/updatableHoverWidget.ts | 12 +- .../browser/view/domLineBreaksComputer.ts | 2 +- src/vs/editor/browser/view/viewLayer.ts | 16 +- .../contentWidgets/contentWidgets.ts | 2 +- .../viewParts/glyphMargin/glyphMargin.ts | 2 +- .../browser/viewParts/viewZones/viewZones.ts | 4 +- .../widget/codeEditor/codeEditorWidget.ts | 16 +- .../components/diffEditorEditors.ts | 16 +- .../diffEditorViewZones.ts | 10 +- .../widget/diffEditor/diffEditorOptions.ts | 2 +- .../widget/diffEditor/diffEditorWidget.ts | 15 +- .../diffEditor/features/gutterFeature.ts | 2 +- .../features/movedBlocksLinesFeature.ts | 4 +- .../diffEditor/registrations.contribution.ts | 4 +- .../editor/browser/widget/diffEditor/utils.ts | 4 +- .../widget/diffEditor/utils/editorGutter.ts | 6 +- .../browser/widget/multiDiffEditor/colors.ts | 2 +- .../multiDiffEditorWidgetImpl.ts | 4 +- src/vs/editor/common/config/editorOptions.ts | 32 +- .../editor/common/core/editorColorRegistry.ts | 86 +- src/vs/editor/common/cursor/cursor.ts | 5 +- .../common/cursor/cursorTypeEditOperations.ts | 1030 ++++++++++++ .../common/cursor/cursorTypeOperations.ts | 984 +---------- .../common/cursor/cursorWordOperations.ts | 11 +- .../editor/common/languageFeatureRegistry.ts | 4 + src/vs/editor/common/languages.ts | 28 +- src/vs/editor/common/languages/autoIndent.ts | 27 +- src/vs/editor/common/model.ts | 5 + .../bracketPairsImpl.ts | 18 +- .../pieceTreeTextBuffer/pieceTreeBase.ts | 21 + .../pieceTreeTextBuffer.ts | 4 + src/vs/editor/common/model/textModel.ts | 7 +- .../common/model/tokenizationTextModelPart.ts | 14 +- .../editor/common/services/getIconClasses.ts | 2 +- .../services/semanticTokensProviderStyling.ts | 10 +- .../common/standalone/standaloneEnums.ts | 131 +- .../editor/common/viewModel/viewModelImpl.ts | 22 +- .../browser/bracketMatching.ts | 2 +- .../test/browser/bracketMatching.test.ts | 2 +- .../browser/codeActionController.ts | 50 +- .../codeAction/browser/lightBulbWidget.css | 1 - .../codeAction/browser/lightBulbWidget.ts | 25 +- .../test/browser/codeAction.test.ts | 2 +- .../codeActionKeybindingResolver.test.ts | 3 +- .../test/browser/codeActionModel.test.ts | 2 +- .../browser/colorHoverParticipant.ts | 46 +- .../colorPicker/browser/colorPickerWidget.ts | 13 +- .../browser/standaloneColorPickerWidget.ts | 20 +- .../test/browser/lineCommentCommand.test.ts | 2 +- .../contextmenu/browser/contextmenu.ts | 2 +- .../test/browser/cursorUndo.test.ts | 2 +- .../test/browser/outlineModel.test.ts | 2 +- .../browser/copyPasteController.ts | 145 +- .../browser/defaultProviders.ts | 14 +- .../browser/dropIntoEditorController.ts | 29 +- .../test/browser/editSort.test.ts | 2 +- .../test/browser/editorState.test.ts | 2 +- .../editor/contrib/find/browser/findWidget.ts | 6 +- .../contrib/find/test/browser/find.test.ts | 2 +- .../find/test/browser/findController.test.ts | 2 +- .../find/test/browser/findModel.test.ts | 2 +- .../find/test/browser/replacePattern.test.ts | 2 +- .../contrib/folding/browser/folding.css | 3 +- .../editor/contrib/folding/browser/folding.ts | 25 + .../folding/browser/foldingDecorations.ts | 3 +- .../folding/test/browser/foldingModel.test.ts | 2 +- .../test/browser/foldingRanges.test.ts | 2 +- .../test/browser/hiddenRangeModel.test.ts | 2 +- .../folding/test/browser/indentFold.test.ts | 2 +- .../test/browser/indentRangeProvider.test.ts | 2 +- .../folding/test/browser/syntaxFold.test.ts | 2 +- .../gotoError/browser/gotoErrorWidget.ts | 2 +- .../browser/peek/referencesWidget.ts | 4 +- .../test/browser/referencesModel.test.ts | 2 +- .../hover/browser/contentHoverController.ts | 403 ++--- .../hover/browser/contentHoverRendered.ts | 436 +++++ .../hover/browser/contentHoverStatusBar.ts | 6 +- .../hover/browser/contentHoverTypes.ts | 31 +- .../hover/browser/contentHoverWidget.ts | 89 +- .../hover/browser/hoverAccessibleViews.ts | 106 +- .../contrib/hover/browser/hoverActions.ts | 14 +- .../contrib/hover/browser/hoverController.ts | 28 +- .../contrib/hover/browser/hoverTypes.ts | 57 +- .../hover/browser/markdownHoverParticipant.ts | 273 +-- .../hover/browser/markerHoverParticipant.ts | 32 +- .../hover/test/browser/contentHover.test.ts | 14 +- .../indentation/browser/indentation.ts | 35 +- .../test/browser/indentation.test.ts | 325 ++-- .../browser/indentationLineProcessor.test.ts | 48 +- .../inlayHints/browser/inlayHintsHover.ts | 4 +- .../browser/ghostTextWidget.ts | 2 +- .../browser/hoverParticipant.ts | 35 +- .../browser/inlineCompletionsController.ts | 255 ++- .../browser/inlineCompletionsHintsWidget.ts | 2 +- .../browser/inlineCompletionsModel.ts | 77 +- .../browser/inlineCompletionsSource.ts | 10 +- .../browser/provideInlineCompletions.ts | 15 +- .../suggestWidgetInlineCompletionProvider.ts | 31 +- .../browser/inlineCompletionsModel.test.ts | 2 +- .../browser/inlineCompletionsProvider.test.ts | 2 +- .../test/browser/suggestWidgetModel.test.ts | 2 +- .../inlineEdit/browser/ghostTextWidget.ts | 2 +- .../inlineEdit/browser/hoverParticipant.ts | 23 +- .../browser/inlineEditController.ts | 8 +- .../browser/inlineEditHintsWidget.ts | 2 +- .../contrib/inlineEdits/browser/commands.ts | 185 +++ .../contrib/inlineEdits/browser/consts.ts | 16 + .../browser/inlineEdits.contribution.ts | 19 + .../browser/inlineEditsController.ts | 97 ++ .../inlineEdits/browser/inlineEditsModel.ts | 289 ++++ .../inlineEdits/browser/inlineEditsWidget.css | 49 + .../inlineEdits/browser/inlineEditsWidget.ts | 400 +++++ .../inlineProgress/browser/inlineProgress.ts | 17 +- .../test/browser/lineSelection.test.ts | 2 +- .../browser/linesOperations.ts | 5 +- .../browser/moveLinesCommand.ts | 103 +- .../test/browser/copyLinesCommand.test.ts | 2 +- .../test/browser/linesOperations.test.ts | 2 +- .../test/browser/linkedEditing.test.ts | 2 +- .../test/browser/multicursor.test.ts | 2 +- .../browser/parameterHintsWidget.ts | 2 +- .../test/browser/parameterHintsModel.test.ts | 2 +- .../contrib/peekView/browser/peekView.ts | 4 +- ...ion.ts => placeholderText.contribution.ts} | 23 +- .../browser/placeholderText.css | 6 +- .../contrib/rename/browser/renameWidget.ts | 6 +- .../browser/documentSemanticTokens.test.ts | 2 +- .../test/browser/getSemanticTokens.test.ts | 2 +- .../test/browser/smartSelect.test.ts | 2 +- .../browser/snippetController2.old.test.ts | 2 +- .../test/browser/snippetController2.test.ts | 2 +- .../test/browser/snippetParser.test.ts | 2 +- .../test/browser/snippetSession.test.ts | 2 +- .../test/browser/snippetVariables.test.ts | 2 +- .../stickyScroll/browser/stickyScroll.css | 2 +- .../browser/stickyScrollController.ts | 67 +- .../browser/stickyScrollWidget.ts | 12 +- .../test/browser/stickyScroll.test.ts | 2 +- .../contrib/suggest/browser/suggestWidget.ts | 18 +- .../suggest/browser/suggestWidgetStatus.ts | 23 +- .../test/browser/completionModel.test.ts | 2 +- .../suggest/test/browser/suggest.test.ts | 2 +- .../test/browser/suggestController.test.ts | 2 +- .../browser/suggestInlineCompletions.test.ts | 2 +- .../test/browser/suggestMemory.test.ts | 2 +- .../suggest/test/browser/suggestModel.test.ts | 2 +- .../suggest/test/browser/wordDistance.test.ts | 2 +- .../symbolIcons/browser/symbolIcons.ts | 191 +-- .../browser/unicodeHighlighter.ts | 10 +- .../browser/highlightDecorations.ts | 10 +- .../wordOperations/browser/wordOperations.ts | 28 +- .../test/browser/wordOperations.test.ts | 36 +- .../browser/wordPartOperations.ts | 6 +- .../test/browser/wordPartOperations.test.ts | 2 +- src/vs/editor/editor.all.ts | 2 + .../standalone/browser/standaloneServices.ts | 2 +- .../common/monarch/monarchCompile.ts | 30 +- .../standalone/test/browser/monarch.test.ts | 2 +- .../test/browser/standaloneLanguages.test.ts | 2 +- .../test/browser/standaloneServices.test.ts | 2 +- .../browser/commands/shiftCommand.test.ts | 2 +- .../test/browser/commands/sideEditing.test.ts | 2 +- .../trimTrailingWhitespaceCommand.test.ts | 2 +- .../config/editorConfiguration.test.ts | 2 +- .../config/editorLayoutProvider.test.ts | 2 +- .../controller/cursor.integrationTest.ts | 2 +- .../test/browser/controller/cursor.test.ts | 2 +- .../controller/cursorMoveCommand.test.ts | 2 +- .../browser/controller/textAreaInput.test.ts | 2 +- .../browser/controller/textAreaState.test.ts | 2 +- .../services/decorationRenderOptions.test.ts | 2 +- .../browser/services/openerService.test.ts | 2 +- src/vs/editor/test/browser/testCommand.ts | 2 +- .../browser/view/minimapCharRenderer.test.ts | 2 +- .../test/browser/view/viewLayer.test.ts | 2 +- .../viewModel/modelLineProjection.test.ts | 2 +- .../test/browser/viewModel/testViewModel.ts | 2 + .../viewModel/viewModelDecorations.test.ts | 2 +- .../browser/viewModel/viewModelImpl.test.ts | 2 +- .../browser/widget/codeEditorWidget.test.ts | 2 +- .../browser/widget/diffEditorWidget.test.ts | 2 +- .../widget/observableCodeEditor.test.ts | 226 +++ .../cursorAtomicMoveOperations.test.ts | 2 +- .../controller/cursorMoveHelper.test.ts | 2 +- .../common/core/characterClassifier.test.ts | 2 +- .../editor/test/common/core/lineRange.test.ts | 2 +- .../test/common/core/lineTokens.test.ts | 2 +- .../core/positionOffsetTransformer.test.ts | 2 +- src/vs/editor/test/common/core/range.test.ts | 2 +- .../test/common/core/stringBuilder.test.ts | 2 +- .../editor/test/common/core/textEdit.test.ts | 2 +- .../test/common/diff/diffComputer.test.ts | 2 +- .../beforeEditPositionMapper.test.ts | 2 +- .../bracketPairColorizer/brackets.test.ts | 2 +- .../combineTextEditInfos.test.ts | 2 +- .../concat23Trees.test.ts | 2 +- .../getBracketPairsInRange.test.ts | 2 +- .../model/bracketPairColorizer/length.test.ts | 2 +- .../smallImmutableSet.test.ts | 2 +- .../bracketPairColorizer/tokenizer.test.ts | 2 +- .../test/common/model/editStack.test.ts | 2 +- .../common/model/editableTextModel.test.ts | 2 +- .../model/editableTextModelTestUtils.ts | 2 +- .../test/common/model/intervalTree.test.ts | 3 +- .../linesTextBuffer/linesTextBuffer.test.ts | 2 +- .../linesTextBufferBuilder.test.ts | 2 +- .../test/common/model/model.line.test.ts | 2 +- .../test/common/model/model.modes.test.ts | 2 +- src/vs/editor/test/common/model/model.test.ts | 2 +- .../common/model/modelDecorations.test.ts | 2 +- .../common/model/modelEditOperation.test.ts | 2 +- .../common/model/modelInjectedText.test.ts | 2 +- .../pieceTreeTextBuffer.test.ts | 18 +- .../test/common/model/textChange.test.ts | 2 +- .../test/common/model/textModel.test.ts | 2 +- .../test/common/model/textModelSearch.test.ts | 2 +- .../test/common/model/textModelTokens.test.ts | 2 +- .../common/model/textModelWithTokens.test.ts | 2 +- .../test/common/model/tokensStore.test.ts | 2 +- .../modes/languageConfiguration.test.ts | 2 +- .../common/modes/languageSelector.test.ts | 2 +- .../test/common/modes/linkComputer.test.ts | 2 +- .../modes/supports/autoClosingPairsRules.ts | 12 +- .../modes/supports/characterPair.test.ts | 2 +- .../modes/supports/electricCharacter.test.ts | 2 +- .../common/modes/supports/onEnter.test.ts | 2 +- .../modes/supports/richEditBrackets.test.ts | 2 +- .../modes/supports/tokenization.test.ts | 2 +- .../common/modes/textToHtmlTokenizer.test.ts | 2 +- .../services/editorSimpleWorker.test.ts | 2 +- .../common/services/languageService.test.ts | 2 +- .../services/languagesAssociations.test.ts | 2 +- .../common/services/languagesRegistry.test.ts | 2 +- .../test/common/services/modelService.test.ts | 2 +- .../common/services/semanticTokensDto.test.ts | 2 +- .../semanticTokensProviderStyling.test.ts | 2 +- .../textResourceConfigurationService.test.ts | 2 +- .../unicodeTextModelHighlighter.test.ts | 2 +- .../common/view/overviewZoneManager.test.ts | 2 +- .../common/viewLayout/lineDecorations.test.ts | 2 +- .../common/viewLayout/linesLayout.test.ts | 2 +- .../viewLayout/viewLineRenderer.test.ts | 2 +- .../common/viewModel/glyphLanesModel.test.ts | 2 +- .../common/viewModel/lineBreakData.test.ts | 2 +- .../monospaceLineBreaksComputer.test.ts | 2 +- .../viewModel/prefixSumComputer.test.ts | 2 +- .../node/classification/typescript.test.ts | 2 +- .../diffing/defaultLinesDiffComputer.test.ts | 2 +- .../editor/test/node/diffing/fixtures.test.ts | 4 +- .../node/diffing/fixtures/issue-214049/1.txt | 2 + .../node/diffing/fixtures/issue-214049/2.txt | 3 + .../issue-214049/advanced.expected.diff.json | 26 + .../issue-214049/legacy.expected.diff.json | 17 + src/vs/monaco.d.ts | 152 +- .../browser/accessibilityService.ts | 29 + .../browser/accessibilitySignalService.ts | 21 +- .../actionWidget/browser/actionWidget.ts | 2 +- src/vs/platform/actions/browser/buttonbar.ts | 55 +- .../platform/actions/browser/floatingMenu.ts | 2 +- .../browser/menuEntryActionViewItem.css | 14 + .../browser/menuEntryActionViewItem.ts | 46 +- src/vs/platform/actions/browser/toolbar.ts | 3 +- src/vs/platform/actions/common/actions.ts | 3 + src/vs/platform/actions/common/menuService.ts | 5 +- .../actions/test/common/menuService.test.ts | 2 +- .../electron-main/backupMainService.test.ts | 2 +- .../test/node/checksumService.test.ts | 2 +- .../clipboard/browser/clipboardService.ts | 2 +- .../commands/test/common/commands.test.ts | 2 +- .../common/configurationRegistry.ts | 164 +- .../test/common/configuration.test.ts | 2 +- .../test/common/configurationModels.test.ts | 2 +- .../test/common/configurationRegistry.test.ts | 34 +- .../test/common/configurationService.test.ts | 2 +- .../test/common/configurations.test.ts | 63 +- .../test/common/policyConfiguration.test.ts | 2 +- .../test/browser/contextkey.test.ts | 2 +- .../contextkey/test/common/contextkey.test.ts | 2 +- .../contextkey/test/common/parser.test.ts | 2 +- .../contextkey/test/common/scanner.test.ts | 2 +- .../diagnostics/common/diagnostics.ts | 2 + .../diagnostics/node/diagnosticsService.ts | 30 +- src/vs/platform/environment/node/argv.ts | 2 +- .../platform/environment/node/argvHelper.ts | 2 +- .../platform/environment/node/userDataPath.js | 6 +- .../environmentMainService.test.ts | 2 +- .../environment/test/node/argv.test.ts | 2 +- .../test/node/environmentService.test.ts | 2 +- .../node/nativeModules.integrationTest.ts | 4 +- .../test/node/userDataPath.test.ts | 2 +- .../abstractExtensionManagementService.ts | 18 +- .../common/extensionGalleryService.ts | 73 +- .../common/extensionManagement.ts | 32 +- .../common/extensionManagementCLI.ts | 4 +- .../common/extensionManagementIpc.ts | 31 +- .../common/extensionsScannerService.ts | 30 +- .../node/extensionManagementService.ts | 81 +- .../extensionSignatureVerificationService.ts | 5 +- .../test/common/configRemotes.test.ts | 2 +- .../common/extensionGalleryService.test.ts | 2 +- .../test/common/extensionManagement.test.ts | 2 +- .../test/common/extensionNls.test.ts | 2 +- .../extensionsProfileScannerService.test.ts | 2 +- .../test/node/extensionDownloader.test.ts | 2 +- .../node/extensionsScannerService.test.ts | 2 +- .../extensions/common/extensionValidator.ts | 47 +- .../platform/extensions/common/extensions.ts | 17 +- .../common/extensionsApiProposals.ts | 378 +++++ .../electron-main/extensionHostStarter.ts | 23 +- .../test/common/extensionValidator.test.ts | 21 +- .../extensions/test/common/extensions.test.ts | 22 + src/vs/platform/files/common/files.ts | 1 + src/vs/platform/files/common/watcher.ts | 68 +- .../files/node/diskFileSystemProvider.ts | 18 +- .../files/node/watcher/baseWatcher.ts | 20 +- .../node/watcher/parcel/parcelWatcher.ts | 48 +- .../files/test/browser/fileService.test.ts | 2 +- .../indexedDBFileService.integrationTest.ts | 2 +- .../platform/files/test/common/files.test.ts | 2 +- .../files/test/common/watcher.test.ts | 2 +- .../node/diskFileService.integrationTest.ts | 2 +- .../node/nodejsWatcher.integrationTest.ts | 6 +- .../node/parcelWatcher.integrationTest.ts | 6 +- .../hover/test/browser/nullHoverService.ts | 4 +- .../instantiation/test/common/graph.test.ts | 2 +- .../test/common/instantiationService.test.ts | 2 +- src/vs/platform/issue/common/issue.ts | 21 +- .../issue/electron-main/issueMainService.ts | 232 +-- .../issue/electron-main/processMainService.ts | 375 +++++ .../common/abstractKeybindingService.test.ts | 2 +- .../test/common/keybindingLabels.test.ts | 2 +- .../test/common/keybindingResolver.test.ts | 2 +- src/vs/platform/list/browser/listService.ts | 2 +- .../markers/test/common/markerService.test.ts | 2 +- .../platform/menubar/electron-main/menubar.ts | 13 +- .../electron-main/menubarMainService.ts | 9 +- .../electron-main/nativeHostMainService.ts | 2 +- .../common/platformObservableUtils.ts | 7 +- src/vs/platform/opener/browser/link.ts | 6 +- .../opener/test/common/opener.test.ts | 2 +- src/vs/platform/product/common/product.ts | 2 +- src/vs/platform/progress/common/progress.ts | 4 +- .../progress/test/common/progress.test.ts | 2 +- .../quickinput/browser/quickInputTree.ts | 12 +- .../platform/quickinput/common/quickAccess.ts | 1 - .../test/browser/quickinput.test.ts | 4 +- .../registry/test/common/platform.test.ts | 2 +- .../remote/common/remoteExtensionsScanner.ts | 2 - .../remote/test/common/remoteHosts.test.ts | 2 +- .../remoteAuthorityResolverService.test.ts | 2 +- src/vs/platform/request/common/request.ts | 6 + .../secrets/test/common/secrets.test.ts | 2 +- src/vs/platform/state/test/node/state.test.ts | 2 +- .../test/browser/1dsAppender.test.ts | 2 +- .../test/browser/telemetryService.test.ts | 4 +- .../test/common/telemetryLogAppender.test.ts | 2 +- .../common/capabilities/capabilities.ts | 8 +- .../commandDetection/promptInputModel.ts | 51 +- .../commandDetectionCapability.ts | 23 +- .../terminal/common/terminalRecorder.ts | 3 +- .../common/xterm/shellIntegrationAddon.ts | 3 +- .../electron-main/electronPtyHostStarter.ts | 2 +- .../platform/terminal/node/ptyHostService.ts | 8 +- .../terminal/node/terminalEnvironment.ts | 4 + .../commandDetection/promptInputModel.test.ts | 25 +- .../test/common/terminalRecorder.test.ts | 2 +- .../test/node/terminalEnvironment.test.ts | 6 +- src/vs/platform/theme/common/colorUtils.ts | 50 +- .../theme/common/colors/baseColors.ts | 4 +- .../theme/common/colors/chartsColors.ts | 12 +- .../theme/common/colors/editorColors.ts | 70 +- .../theme/common/colors/inputColors.ts | 18 +- .../theme/common/colors/listColors.ts | 22 +- .../theme/common/colors/menuColors.ts | 8 +- .../theme/common/colors/minimapColors.ts | 10 +- .../theme/common/colors/miscColors.ts | 2 +- .../theme/common/colors/quickpickColors.ts | 10 +- src/vs/platform/tunnel/common/tunnel.ts | 9 +- .../tunnel/test/common/tunnel.test.ts | 2 +- .../test/common/undoRedoService.test.ts | 2 +- .../test/common/uriIdentityService.test.ts | 2 +- .../test/browser/fileUserDataProvider.test.ts | 2 +- .../userDataProfile/common/userDataProfile.ts | 107 +- .../common/userDataProfileStorageService.ts | 65 +- .../userDataProfileStorageService.ts | 2 +- .../node/userDataProfileStorageService.ts | 4 +- .../common/userDataProfileService.test.ts | 2 +- .../userDataProfileStorageService.test.ts | 5 +- .../userDataProfileMainService.test.ts | 2 +- .../userDataSync/common/extensionsSync.ts | 2 +- .../test/common/extensionsMerge.test.ts | 2 +- .../test/common/globalStateMerge.test.ts | 2 +- .../test/common/globalStateSync.test.ts | 2 +- .../test/common/keybindingsMerge.test.ts | 2 +- .../test/common/keybindingsSync.test.ts | 2 +- .../test/common/settingsMerge.test.ts | 2 +- .../test/common/settingsSync.test.ts | 2 +- .../test/common/snippetsMerge.test.ts | 2 +- .../test/common/snippetsSync.test.ts | 2 +- .../test/common/synchronizer.test.ts | 2 +- .../test/common/tasksSync.test.ts | 2 +- .../common/userDataAutoSyncService.test.ts | 2 +- .../userDataProfilesManifestMerge.test.ts | 2 +- .../userDataProfilesManifestSync.test.ts | 2 +- .../test/common/userDataSyncClient.ts | 2 +- .../test/common/userDataSyncService.test.ts | 2 +- .../common/userDataSyncStoreService.test.ts | 2 +- src/vs/platform/window/common/window.ts | 6 +- .../windows/electron-main/windowImpl.ts | 5 +- .../electron-main/windowsMainService.ts | 2 +- .../electron-main/windowsStateHandler.ts | 10 +- .../test/electron-main/windowsFinder.test.ts | 2 +- .../electron-main/windowsStateHandler.test.ts | 2 +- .../workspace/test/common/workspace.test.ts | 2 +- .../workspaces/test/common/workspaces.test.ts | 2 +- .../test/electron-main/workspaces.test.ts | 2 +- .../workspacesHistoryStorage.test.ts | 2 +- .../workspacesManagementMainService.test.ts | 2 +- src/vs/server/node/extensionHostConnection.ts | 34 +- .../node/remoteExtensionHostAgentServer.ts | 1 + src/vs/server/node/remoteExtensionsScanner.ts | 31 - .../test/node/serverConnectionToken.test.ts | 2 +- .../api/browser/extensionHost.contribution.ts | 1 + .../api/browser/mainThreadAuthentication.ts | 38 +- .../api/browser/mainThreadChatAgents2.ts | 3 +- .../api/browser/mainThreadChatVariables.ts | 7 - .../api/browser/mainThreadComments.ts | 31 +- .../api/browser/mainThreadDebugService.ts | 1 + .../workbench/api/browser/mainThreadErrors.ts | 8 +- .../api/browser/mainThreadExtensionService.ts | 12 +- .../api/browser/mainThreadLanguageFeatures.ts | 26 +- .../browser/mainThreadLanguageModelTools.ts | 50 + .../api/browser/mainThreadLanguageModels.ts | 91 +- src/vs/workbench/api/browser/mainThreadSCM.ts | 120 +- .../api/browser/mainThreadTesting.ts | 2 +- .../workbench/api/common/extHost.api.impl.ts | 50 +- .../api/common/extHost.common.services.ts | 2 + .../workbench/api/common/extHost.protocol.ts | 87 +- .../api/common/extHostAuthentication.ts | 18 +- .../api/common/extHostChatAgents2.ts | 22 +- .../api/common/extHostChatVariables.ts | 4 - .../workbench/api/common/extHostComments.ts | 12 +- .../api/common/extHostDebugService.ts | 39 +- src/vs/workbench/api/common/extHostDialogs.ts | 4 +- .../api/common/extHostExtensionService.ts | 13 +- .../api/common/extHostLanguageFeatures.ts | 92 +- .../api/common/extHostLanguageModelTools.ts | 70 + .../api/common/extHostLanguageModels.ts | 166 +- .../workbench/api/common/extHostNotebook.ts | 4 +- src/vs/workbench/api/common/extHostSCM.ts | 91 +- src/vs/workbench/api/common/extHostSearch.ts | 6 +- src/vs/workbench/api/common/extHostSecrets.ts | 22 +- .../api/common/extHostTerminalService.ts | 13 +- src/vs/workbench/api/common/extHostTesting.ts | 180 +- .../api/common/extHostTextEditors.ts | 9 +- .../api/common/extHostTypeConverters.ts | 81 +- src/vs/workbench/api/common/extHostTypes.ts | 42 +- .../workbench/api/common/extHostWorkspace.ts | 2 +- .../workbench/api/common/extensionHostMain.ts | 4 +- .../workbench/api/node/extHostDebugService.ts | 8 +- src/vs/workbench/api/node/extHostSearch.ts | 50 +- .../api/node/extensionHostProcess.ts | 27 +- src/vs/workbench/api/node/proxyResolver.ts | 19 +- .../api/test/browser/extHost.api.impl.test.ts | 2 +- .../test/browser/extHostApiCommands.test.ts | 18 +- .../extHostAuthentication.integrationTest.ts | 2 +- .../api/test/browser/extHostBulkEdits.test.ts | 2 +- .../api/test/browser/extHostCommands.test.ts | 2 +- .../test/browser/extHostConfiguration.test.ts | 2 +- .../test/browser/extHostDecorations.test.ts | 2 +- .../test/browser/extHostDiagnostics.test.ts | 2 +- .../extHostDocumentContentProvider.test.ts | 2 +- .../test/browser/extHostDocumentData.test.ts | 2 +- .../extHostDocumentSaveParticipant.test.ts | 2 +- .../extHostDocumentsAndEditors.test.ts | 2 +- .../test/browser/extHostEditorTabs.test.ts | 2 +- .../extHostFileSystemEventService.test.ts | 2 +- .../browser/extHostLanguageFeatures.test.ts | 2 +- .../browser/extHostMessagerService.test.ts | 2 +- .../api/test/browser/extHostNotebook.test.ts | 2 +- .../browser/extHostNotebookKernel.test.ts | 3 +- .../api/test/browser/extHostTelemetry.test.ts | 5 +- .../api/test/browser/extHostTesting.test.ts | 31 +- .../test/browser/extHostTextEditor.test.ts | 2 +- .../api/test/browser/extHostTreeViews.test.ts | 2 +- .../test/browser/extHostTypeConverter.test.ts | 2 +- .../api/test/browser/extHostTypes.test.ts | 2 +- .../api/test/browser/extHostWebview.test.ts | 2 +- .../api/test/browser/extHostWorkspace.test.ts | 2 +- .../test/browser/mainThreadBulkEdits.test.ts | 2 +- .../test/browser/mainThreadCommands.test.ts | 2 +- .../browser/mainThreadConfiguration.test.ts | 2 +- .../browser/mainThreadDiagnostics.test.ts | 2 +- ...mainThreadDocumentContentProviders.test.ts | 2 +- .../test/browser/mainThreadDocuments.test.ts | 2 +- .../mainThreadDocumentsAndEditors.test.ts | 2 +- .../test/browser/mainThreadEditors.test.ts | 2 +- .../browser/mainThreadManagedSockets.test.ts | 2 +- .../test/browser/mainThreadTreeViews.test.ts | 2 +- .../test/browser/mainThreadWorkspace.test.ts | 2 +- .../common/extHostExtensionActivator.test.ts | 14 +- .../api/test/common/extensionHostMain.test.ts | 6 +- .../api/test/node/extHostSearch.test.ts | 23 +- .../test/node/extHostTunnelService.test.ts | 2 +- .../browser/actions/developerActions.ts | 2 +- .../workbench/browser/actions/listCommands.ts | 2 +- src/vs/workbench/browser/layout.ts | 58 +- src/vs/workbench/browser/media/style.css | 9 + .../activitybar/media/activityaction.css | 3 +- .../workbench/browser/parts/compositePart.ts | 2 +- .../browser/parts/editor/editorActions.ts | 34 +- .../browser/parts/editor/editorCommands.ts | 4 +- .../browser/parts/editor/editorDropTarget.ts | 2 +- .../browser/parts/editor/editorPanes.ts | 2 +- .../browser/parts/editor/editorParts.ts | 2 +- .../browser/parts/editor/editorStatus.ts | 22 +- .../browser/parts/editor/sideBySideEditor.ts | 2 +- .../browser/parts/globalCompositeBar.ts | 10 +- .../notifications/notificationsToasts.ts | 2 +- .../notifications/notificationsViewer.ts | 10 +- .../parts/statusbar/media/statusbarpart.css | 5 + .../browser/parts/statusbar/statusbarItem.ts | 12 +- .../browser/parts/statusbar/statusbarModel.ts | 32 +- .../browser/parts/statusbar/statusbarPart.ts | 27 +- .../parts/titlebar/commandCenterControl.ts | 4 +- .../browser/parts/titlebar/titlebarPart.ts | 2 +- .../workbench/browser/parts/views/checkbox.ts | 8 +- .../workbench/browser/parts/views/treeView.ts | 40 +- .../browser/parts/views/viewFilter.ts | 2 + .../workbench/browser/parts/views/viewPane.ts | 14 +- .../browser/parts/views/viewPaneContainer.ts | 2 +- .../browser/workbench.contribution.ts | 7 +- src/vs/workbench/common/configuration.ts | 2 +- src/vs/workbench/common/contributions.ts | 11 +- src/vs/workbench/common/theme.ts | 563 ++----- .../browser/accessibilityConfiguration.ts | 223 ++- ...accessibilitySignalDebuggerContribution.ts | 2 +- .../editorTextPropertySignalsContribution.ts | 4 +- .../test/browser/bulkCellEdits.test.ts | 2 +- .../test/browser/bulkEditPreview.test.ts | 2 +- .../chat/browser/actions/chatActions.ts | 34 +- .../browser/actions/chatCodeblockActions.ts | 71 +- .../browser/actions/chatContextActions.ts | 73 +- .../browser/actions/chatDeveloperActions.ts | 35 + .../browser/actions/chatExecuteActions.ts | 1 + .../contrib/chat/browser/chat.contribution.ts | 24 +- src/vs/workbench/contrib/chat/browser/chat.ts | 17 +- .../contrib/chat/browser/chatAgentHover.ts | 9 +- .../chatContentParts/chatCollections.ts | 43 + .../chatCommandContentPart.ts | 46 + .../chatConfirmationContentPart.ts | 64 + .../chatConfirmationWidget.ts | 0 .../chatContentParts/chatContentParts.ts | 26 + .../chatMarkdownContentPart.ts | 176 ++ .../chatProgressContentPart.ts | 65 + .../chatReferencesContentPart.ts | 299 ++++ .../chatContentParts/chatTaskContentPart.ts | 54 + .../chatTextEditContentPart.ts | 211 +++ .../chatContentParts/chatTreeContentPart.ts | 225 +++ .../chatWarningContentPart.ts | 53 + .../media/chatConfirmationWidget.css | 0 .../contrib/chat/browser/chatInputPart.ts | 142 +- .../contrib/chat/browser/chatListRenderer.ts | 1466 ++++------------- .../chatMarkdownDecorationsRenderer.ts | 4 +- .../chat/browser/chatMarkdownRenderer.ts | 3 +- .../browser/chatParticipantContributions.ts | 14 +- .../contrib/chat/browser/chatVariables.ts | 6 +- .../contrib/chat/browser/chatWidget.ts | 39 +- .../contrib/chat/browser/codeBlockPart.css | 14 + .../contrib/chat/browser/codeBlockPart.ts | 121 +- .../browser/contrib/chatContextAttachments.ts | 17 +- .../browser/contrib/chatDynamicVariables.ts | 30 +- .../browser/contrib/chatInputCompletions.ts | 36 +- .../browser/contrib/chatInputEditorContrib.ts | 5 +- .../browser/contrib/chatInputEditorHover.ts | 100 ++ .../browser/contrib/editorHoverWrapper.ts | 52 + .../contrib/media/editorHoverWrapper.css | 8 + .../contrib/chat/browser/media/chat.css | 44 +- .../chat/browser/media/chatAgentHover.css | 10 +- .../contrib/chat/common/annotations.ts | 2 +- .../contrib/chat/common/chatAgents.ts | 11 + .../contrib/chat/common/chatColors.ts | 2 +- .../contrib/chat/common/chatModel.ts | 5 +- .../contrib/chat/common/chatServiceImpl.ts | 12 +- .../contrib/chat/common/chatViewModel.ts | 30 +- .../contrib/chat/common/chatWordCounter.ts | 12 +- .../chat/common/codeBlockModelCollection.ts | 26 + .../chat/common/languageModelToolsService.ts | 110 ++ .../contrib/chat/common/languageModels.ts | 46 +- .../tools/languageModelToolsContribution.ts | 94 ++ .../ChatMarkdownRenderer_invalid_HTML.0.snap | 2 +- ...nderer_invalid_HTML_with_attributes.0.snap | 2 +- .../ChatMarkdownRenderer_remote_images.0.snap | 2 +- ...kdownRenderer_self-closing_elements.0.snap | 2 +- ..._supportHtml_with_one-line_markdown.0.snap | 1 + ..._supportHtml_with_one-line_markdown.1.snap | 4 + .../test/browser/chatMarkdownRenderer.test.ts | 12 + .../chat/test/browser/chatVariables.test.ts | 2 +- .../chat/test/common/chatAgents.test.ts | 2 +- .../chat/test/common/chatModel.test.ts | 2 +- .../chat/test/common/chatService.test.ts | 2 +- .../chat/test/common/chatWordCounter.test.ts | 4 +- .../chat/test/common/languageModels.test.ts | 61 +- .../chat/test/common/voiceChatService.test.ts | 3 +- .../electron-sandbox/voiceChatActions.test.ts | 2 +- .../browser/codeActionsContribution.ts | 48 +- .../codeEditor/browser/diffEditorHelper.ts | 2 +- .../emptyTextEditorHint.ts | 74 +- .../browser/find/simpleFindWidget.ts | 4 +- .../suggestEnabledInput.ts | 2 +- .../browser/toggleMultiCursorModifier.ts | 16 +- .../codeEditor/browser/toggleWordWrap.ts | 16 +- .../test/browser/saveParticipant.test.ts | 2 +- .../codeEditor/test/node/autoindent.test.ts | 2 +- .../contrib/comments/browser/commentColors.ts | 6 +- .../comments/browser/commentGlyphWidget.ts | 6 +- .../contrib/comments/browser/commentReply.ts | 4 +- .../comments/browser/commentThreadBody.ts | 2 +- .../comments/browser/commentThreadHeader.ts | 3 +- .../comments/browser/commentThreadWidget.ts | 3 +- .../browser/commentThreadZoneWidget.ts | 4 +- .../comments/browser/commentsController.ts | 14 +- .../comments/browser/commentsTreeViewer.ts | 2 +- .../contrib/comments/browser/media/panel.css | 5 +- .../contrib/comments/browser/media/review.css | 5 - .../contrib/comments/browser/timestamp.ts | 6 +- .../test/browser/commentsView.test.ts | 2 +- .../browser/contextmenu.contribution.ts | 12 +- .../contrib/debug/browser/baseDebugView.ts | 25 +- .../browser/breakpointEditorContribution.ts | 8 +- .../contrib/debug/browser/breakpointWidget.ts | 19 +- .../contrib/debug/browser/breakpointsView.ts | 33 +- .../contrib/debug/browser/callStackView.ts | 14 +- .../debug/browser/debug.contribution.ts | 9 +- .../debug/browser/debugActionViewItems.ts | 2 +- .../contrib/debug/browser/debugColors.ts | 25 +- .../debug/browser/debugEditorActions.ts | 12 +- .../debug/browser/debugEditorContribution.ts | 72 +- .../contrib/debug/browser/debugHover.ts | 50 +- .../contrib/debug/browser/debugService.ts | 21 +- .../contrib/debug/browser/debugSession.ts | 33 +- .../contrib/debug/browser/debugToolBar.ts | 6 +- .../contrib/debug/browser/exceptionWidget.ts | 2 +- .../debug/browser/media/debugToolBar.css | 2 +- .../contrib/debug/browser/media/repl.css | 5 +- .../workbench/contrib/debug/browser/repl.ts | 13 +- .../contrib/debug/browser/replViewer.ts | 97 +- .../debug/browser/statusbarColorProvider.ts | 24 +- .../contrib/debug/browser/variablesView.ts | 13 +- .../debug/browser/watchExpressionsView.ts | 35 +- .../workbench/contrib/debug/common/debug.ts | 31 +- .../contrib/debug/common/debugLifecycle.ts | 9 +- .../contrib/debug/common/debugModel.ts | 27 +- .../contrib/debug/common/debugProtocol.d.ts | 11 +- .../contrib/debug/common/debugVisualizers.ts | 4 +- .../contrib/debug/common/replModel.ts | 8 +- .../debug/test/browser/baseDebugView.test.ts | 127 +- .../debug/test/browser/breakpoints.test.ts | 2 +- .../debug/test/browser/callStack.test.ts | 6 +- .../test/browser/debugANSIHandling.test.ts | 2 +- .../browser/debugConfigurationManager.test.ts | 2 +- .../debug/test/browser/debugHover.test.ts | 2 +- .../debug/test/browser/debugMemory.test.ts | 2 +- .../debug/test/browser/debugSession.test.ts | 2 +- .../debug/test/browser/debugSource.test.ts | 2 +- .../debug/test/browser/debugUtils.test.ts | 2 +- .../debug/test/browser/debugViewModel.test.ts | 2 +- .../debug/test/browser/linkDetector.test.ts | 2 +- .../test/browser/rawDebugSession.test.ts | 2 +- .../contrib/debug/test/browser/repl.test.ts | 2 +- .../debug/test/browser/variablesView.test.ts | 118 ++ .../contrib/debug/test/browser/watch.test.ts | 2 +- .../test/browser/watchExpressionView.test.ts | 114 ++ .../test/common/abstractDebugAdapter.test.ts | 2 +- .../debug/test/common/debugModel.test.ts | 2 +- .../contrib/debug/test/node/debugger.test.ts | 11 +- .../test/node/streamDebugAdapter.test.ts | 2 +- .../contrib/debug/test/node/terminals.test.ts | 2 +- .../test/browser/editSessions.test.ts | 2 +- .../emmet/test/browser/emmetAction.test.ts | 2 +- .../encryption.contribution.ts | 4 +- .../abstractRuntimeExtensionsEditor.ts | 6 +- .../extensions/browser/extensionEditor.ts | 48 +- ...ensionRecommendationNotificationService.ts | 12 +- .../browser/extensions.contribution.ts | 134 +- .../extensions/browser/extensionsActions.ts | 267 +-- .../extensions/browser/extensionsList.ts | 45 +- .../extensions/browser/extensionsViewlet.ts | 7 +- .../extensions/browser/extensionsWidgets.ts | 34 +- .../browser/extensionsWorkbenchService.ts | 97 +- .../browser/media/extensionActions.css | 15 +- .../browser/media/extensionEditor.css | 1 + .../browser/media/extensionsWidgets.css | 1 + .../contrib/extensions/common/extensions.ts | 1 + .../test/common/extensionQuery.test.ts | 2 +- .../test/electron-sandbox/extension.test.ts | 2 +- .../extensionRecommendationsService.test.ts | 2 +- .../extensionsActions.test.ts | 170 +- .../electron-sandbox/extensionsViews.test.ts | 7 +- .../extensionsWorkbenchService.test.ts | 36 +- .../browser/externalTerminal.contribution.ts | 4 +- .../common/externalUriOpenerService.test.ts | 2 +- .../contrib/files/browser/fileActions.ts | 24 +- .../files/browser/files.contribution.ts | 17 +- .../files/browser/views/openEditorsView.ts | 4 +- .../files/test/browser/editorAutoSave.test.ts | 2 +- .../browser/explorerFileNestingTrie.test.ts | 2 +- .../files/test/browser/explorerModel.test.ts | 2 +- .../files/test/browser/explorerView.test.ts | 2 +- .../files/test/browser/fileActions.test.ts | 2 +- .../test/browser/fileEditorInput.test.ts | 2 +- .../test/browser/fileOnDiskProvider.test.ts | 2 +- .../browser/textFileEditorTracker.test.ts | 2 +- .../browser/inlineChat.contribution.ts | 90 +- .../browser/inlineChatAccessibleView.ts | 4 +- .../inlineChat/browser/inlineChatActions.ts | 200 ++- .../browser/inlineChatContentWidget.ts | 55 +- .../browser/inlineChatController.ts | 194 +-- .../browser/inlineChatFileCreationWidget.ts | 256 --- .../inlineChat/browser/inlineChatSession.ts | 84 +- .../browser/inlineChatSessionService.ts | 7 +- .../browser/inlineChatSessionServiceImpl.ts | 41 +- .../browser/inlineChatStrategies.ts | 11 +- .../inlineChat/browser/inlineChatWidget.ts | 170 +- .../browser/inlineChatZoneWidget.ts | 147 +- .../inlineChat/browser/media/inlineChat.css | 95 +- .../browser/media/inlineChatContentWidget.css | 19 +- .../contrib/inlineChat/common/inlineChat.ts | 182 +- .../test/browser/inlineChatController.test.ts | 200 ++- .../test/browser/inlineChatSession.test.ts | 2 +- .../test/browser/inlineChatStrategies.test.ts | 2 +- .../browser/interactive.contribution.ts | 104 +- .../interactive/browser/interactiveCommon.ts | 4 +- .../interactive/browser/interactiveEditor.ts | 102 +- .../browser/replInputHintContentWidget.ts | 156 ++ .../workbench/contrib/issue/browser/issue.ts | 26 +- .../contrib/issue/browser/issueQuickAccess.ts | 4 +- .../issue/browser/issueReporterService.ts | 14 + .../workbench/contrib/issue/common/issue.ts | 7 + .../electron-sandbox/issue.contribution.ts | 91 +- .../electron-sandbox/issueMainService.ts | 4 +- .../issue/electron-sandbox/issueReporter.js | 8 +- .../electron-sandbox/issueReporterMain.ts | 4 +- .../electron-sandbox/issueReporterService.ts | 15 +- .../electron-sandbox/issueReporterService2.ts | 35 +- .../issue/electron-sandbox/issueService.ts | 37 +- .../electron-sandbox/process.contribution.ts | 96 ++ .../issue/electron-sandbox/processService.ts | 62 + .../issue/issue/testReporterModel.test.ts | 2 +- .../browser/languageStatus.contribution.ts | 2 +- .../browser/media/languageStatus.css | 1 + .../contrib/logs/common/logs.contribution.ts | 4 +- .../browser/markdownDocumentRenderer.ts | 28 +- .../browser/markdownSettingRenderer.test.ts | 2 +- .../markers/browser/markersFileDecorations.ts | 2 +- .../markers/browser/markersTreeViewer.ts | 6 +- .../markers/test/browser/markersModel.test.ts | 2 +- .../browser/mergeEditorInputModel.ts | 4 +- .../mergeEditor/browser/view/colors.ts | 18 +- .../mergeEditor/browser/view/editorGutter.ts | 7 +- .../browser/view/editors/codeEditorView.ts | 6 +- .../mergeEditor/test/browser/mapping.test.ts | 2 +- .../mergeEditor/test/browser/model.test.ts | 2 +- .../browser/scmMultiDiffSourceResolver.ts | 4 +- .../executionStatusBarItemController.ts | 16 +- .../contrib/editorHint/emptyCellEditorHint.ts | 7 +- .../editorStatusBar/editorStatusBar.ts | 43 +- .../browser/contrib/find/findFilters.ts | 43 +- .../contrib/find/findMatchDecorationModel.ts | 6 +- .../browser/contrib/find/findModel.ts | 24 +- .../contrib/find/media/notebookFind.css | 4 + .../browser/contrib/find/notebookFind.ts | 13 +- .../contrib/find/notebookFindReplaceWidget.ts | 170 +- .../contrib/find/notebookFindWidget.ts | 9 +- .../browser/contrib/navigation/arrow.ts | 18 +- .../contrib/outline/notebookOutline.ts | 412 +++-- .../saveParticipants/saveParticipants.ts | 2 +- .../browser/contrib/troubleshoot/layout.ts | 4 +- .../notebook/browser/controller/apiActions.ts | 2 +- .../browser/controller/cellOutputActions.ts | 47 +- .../controller/chat/cellChatActions.ts | 21 +- .../chat/notebook.chat.contribution.ts | 25 +- .../controller/chat/notebookChatContext.ts | 2 + .../controller/chat/notebookChatController.ts | 25 +- .../browser/diff/diffElementOutputs.ts | 4 +- .../browser/diff/notebookDiffEditor.ts | 6 +- .../browser/media/notebookCellChat.css | 9 - .../notebook/browser/notebook.contribution.ts | 15 +- .../browser/notebookAccessibilityProvider.ts | 2 +- .../notebook/browser/notebookBrowser.ts | 11 +- .../notebook/browser/notebookEditorWidget.ts | 159 +- .../contrib/notebook/browser/notebookIcons.ts | 1 + .../notebook/browser/notebookOptions.ts | 8 +- .../services/notebookEditorServiceImpl.ts | 13 +- .../notebookKernelHistoryServiceImpl.ts | 2 +- .../services/notebookKernelServiceImpl.ts | 14 +- .../browser/services/notebookServiceImpl.ts | 16 +- .../browser/view/cellParts/cellActionView.ts | 6 +- .../browser/view/cellParts/cellComments.ts | 98 +- .../browser/view/cellParts/cellDnd.ts | 2 +- .../browser/view/cellParts/cellOutput.ts | 10 +- .../browser/view/cellParts/cellStatusPart.ts | 23 +- .../browser/view/cellParts/cellToolbars.ts | 34 +- .../browser/view/cellParts/codeCell.ts | 4 +- .../view/cellParts/codeCellRunToolbar.ts | 15 +- .../browser/view/cellParts/markupCell.ts | 2 +- .../view/renderers/backLayerWebView.ts | 10 +- .../browser/view/renderers/cellRenderer.ts | 6 +- .../browser/view/renderers/webviewPreloads.ts | 2 +- .../browser/viewModel/baseCellViewModel.ts | 28 +- .../browser/viewModel/codeCellViewModel.ts | 18 +- .../browser/viewModel/markupCellViewModel.ts | 4 +- .../viewModel/notebookOutlineDataSource.ts | 222 +++ .../notebookOutlineDataSourceFactory.ts | 38 + .../viewModel/notebookOutlineEntryFactory.ts | 3 +- .../viewModel/notebookOutlineProvider.ts | 316 ---- .../notebookOutlineProviderFactory.ts | 39 - .../viewModel/notebookViewModelImpl.ts | 35 +- .../viewParts/notebookEditorStickyScroll.ts | 29 +- .../viewParts/notebookEditorToolbar.ts | 2 +- .../notebookKernelQuickPickStrategy.ts | 4 +- .../viewParts/notebookTopCellToolbar.ts | 8 +- .../common/model/notebookTextModel.ts | 4 + .../contrib/notebook/common/notebookCommon.ts | 45 +- .../notebook/common/notebookContextKeys.ts | 6 +- .../notebook/common/notebookEditorInput.ts | 73 +- .../notebook/common/notebookEditorModel.ts | 28 +- .../notebookEditorModelResolverService.ts | 7 +- .../notebookEditorModelResolverServiceImpl.ts | 24 +- .../notebook/common/notebookKernelService.ts | 2 +- .../notebook/common/notebookProvider.ts | 3 - .../notebook/common/notebookService.ts | 1 + .../test/browser/cellDecorations.test.ts | 2 +- .../notebook/test/browser/cellDnd.test.ts | 2 +- .../test/browser/cellOperations.test.ts | 2 +- ...contributedStatusBarItemController.test.ts | 2 +- .../contrib/executionStatusBarItem.test.ts | 2 +- .../test/browser/contrib/find.test.ts | 2 +- .../browser/contrib/layoutActions.test.ts | 2 +- .../contrib/notebookCellDiagnostics.test.ts | 2 +- .../browser/contrib/notebookClipboard.test.ts | 2 +- .../browser/contrib/notebookOutline.test.ts | 7 +- .../notebookOutlineViewProviders.test.ts | 63 +- .../browser/contrib/notebookSymbols.test.ts | 13 +- .../browser/contrib/notebookUndoRedo.test.ts | 2 +- .../browser/contrib/outputCopyTests.test.ts | 2 +- .../test/browser/notebookBrowser.test.ts | 2 +- .../test/browser/notebookCellAnchor.test.ts | 2 +- .../test/browser/notebookCellList.test.ts | 2 +- .../test/browser/notebookCommon.test.ts | 2 +- .../test/browser/notebookDiff.test.ts | 2 +- .../test/browser/notebookEditor.test.ts | 2 +- .../test/browser/notebookEditorModel.test.ts | 48 +- .../browser/notebookExecutionService.test.ts | 30 +- .../notebookExecutionStateService.test.ts | 57 +- .../test/browser/notebookFolding.test.ts | 2 +- .../browser/notebookKernelHistory.test.ts | 26 +- .../browser/notebookKernelService.test.ts | 26 +- .../notebookRendererMessagingService.test.ts | 2 +- .../test/browser/notebookSelection.test.ts | 2 +- .../test/browser/notebookServiceImpl.test.ts | 4 +- .../test/browser/notebookStickyScroll.test.ts | 13 +- .../test/browser/notebookTextModel.test.ts | 2 +- .../notebookVariablesDataSource.test.ts | 2 +- .../test/browser/notebookViewModel.test.ts | 4 +- .../test/browser/notebookViewZones.test.ts | 2 +- .../browser/notebookWorkbenchToolbar.test.ts | 2 +- .../test/browser/testNotebookEditor.ts | 18 +- .../outline/browser/outline.contribution.ts | 6 +- .../output/browser/outputLinkProvider.ts | 8 +- .../contrib/output/browser/outputServices.ts | 4 +- .../test/browser/outputLinkProvider.test.ts | 2 +- .../electron-sandbox/startupTimings.ts | 10 +- .../browser/PositronNotebookInstance.ts | 5 +- .../preferences/browser/keybindingsEditor.ts | 16 +- .../browser/media/settingsEditor2.css | 8 +- .../browser/preferences.contribution.ts | 1 + .../browser/preferencesRenderers.ts | 1 + .../preferences/browser/preferencesWidgets.ts | 6 +- .../preferences/browser/settingsEditor2.ts | 15 +- .../settingsEditorSettingIndicators.ts | 50 +- .../preferences/browser/settingsTree.ts | 186 ++- .../preferences/browser/settingsTreeModels.ts | 24 +- .../preferences/browser/settingsWidgets.ts | 200 ++- .../contrib/preferences/browser/tocTree.ts | 2 +- .../common/settingsEditorColorRegistry.ts | 39 +- .../keybindingsEditorContribution.test.ts | 2 +- .../test/browser/settingsTreeModels.test.ts | 2 +- .../test/common/smartSnippetInserter.test.ts | 2 +- .../remote/browser/media/tunnelView.css | 5 + .../contrib/remote/browser/remote.ts | 6 +- .../contrib/remote/browser/tunnelView.ts | 16 +- .../browser/interactiveEditor.css | 21 + .../browser/media/interactive.css | 36 + .../replNotebook/browser/repl.contribution.ts | 260 +++ .../replNotebook/browser/replEditor.ts | 725 ++++++++ .../replNotebook/browser/replEditorInput.ts | 88 + .../workbench/contrib/scm/browser/activity.ts | 320 ++-- .../contrib/scm/browser/dirtydiffDecorator.ts | 42 +- .../contrib/scm/browser/media/scm.css | 33 +- src/vs/workbench/contrib/scm/browser/menus.ts | 2 +- .../contrib/scm/browser/scm.contribution.ts | 30 +- .../contrib/scm/browser/scmHistory.ts | 261 +++ .../scm/browser/scmRepositoryRenderer.ts | 40 +- .../contrib/scm/browser/scmViewPane.ts | 334 +++- src/vs/workbench/contrib/scm/browser/util.ts | 10 +- .../workbench/contrib/scm/common/history.ts | 31 +- src/vs/workbench/contrib/scm/common/scm.ts | 12 +- .../contrib/scm/common/scmService.ts | 51 +- .../scm/test/browser/scmHistory.test.ts | 503 ++++++ .../search/browser/anythingQuickAccess.ts | 7 +- .../notebookSearch/notebookSearchService.ts | 4 +- .../search/browser/search.contribution.ts | 13 +- .../search/browser/searchActionsFind.ts | 5 + .../contrib/search/browser/searchModel.ts | 6 +- .../search/browser/searchResultsView.ts | 6 +- .../contrib/search/browser/searchView.ts | 43 +- .../contrib/search/browser/searchWidget.ts | 33 +- .../search/test/browser/searchActions.test.ts | 2 +- .../search/test/browser/searchModel.test.ts | 3 +- .../browser/searchNotebookHelpers.test.ts | 2 +- .../search/test/browser/searchResult.test.ts | 2 +- .../search/test/browser/searchViewlet.test.ts | 2 +- .../search/test/common/cacheState.test.ts | 2 +- .../search/test/common/extractRange.test.ts | 3 +- .../searchEditor/browser/searchEditor.ts | 6 +- .../snippets/test/browser/snippetFile.test.ts | 2 +- .../test/browser/snippetsRegistry.test.ts | 2 +- .../test/browser/snippetsRewrite.test.ts | 2 +- .../test/browser/snippetsService.test.ts | 2 +- .../speech/test/common/speechService.test.ts | 2 +- .../contrib/splash/browser/partsSplash.ts | 4 +- .../electron-sandbox/workspaceTagsService.ts | 163 +- .../tags/test/node/workspaceTags.test.ts | 2 +- .../tasks/browser/abstractTaskService.ts | 2 +- .../tasks/browser/task.contribution.ts | 29 +- .../tasks/test/common/problemMatcher.test.ts | 2 +- .../test/common/taskConfiguration.test.ts | 2 +- .../browser/telemetry.contribution.ts | 15 +- .../browser/environmentVariableInfo.ts | 4 +- .../browser/media/shellIntegration-bash.sh | 37 +- .../browser/media/shellIntegration.ps1 | 36 +- .../terminal/browser/terminal.contribution.ts | 2 +- .../contrib/terminal/browser/terminalGroup.ts | 4 +- .../terminal/browser/terminalInstance.ts | 112 +- .../browser/terminalProcessManager.ts | 4 +- .../browser/terminalProfileResolverService.ts | 11 +- .../browser/terminalProfileService.ts | 2 +- .../browser/terminalRunRecentQuickPick.ts | 3 + .../terminal/browser/terminalService.ts | 48 +- .../terminal/browser/terminalTabbedView.ts | 4 +- .../terminal/browser/terminalTabsList.ts | 6 +- .../contrib/terminal/browser/terminalView.ts | 2 +- .../terminal/browser/widgets/widgetManager.ts | 4 +- .../terminal/browser/xterm/xtermTerminal.ts | 25 +- .../contrib/terminal/common/terminal.ts | 2 - .../terminal/common/terminalColorRegistry.ts | 48 +- .../test/common/terminalColorRegistry.test.ts | 2 +- .../test/common/terminalDataBuffering.test.ts | 2 +- .../test/browser/bufferContentTracker.test.ts | 2 +- .../browser/media/terminalInitialHint.css | 3 +- .../browser/terminal.chat.contribution.ts | 4 + .../terminal.initialHint.contribution.ts | 67 +- .../chat/browser/terminalChat.ts | 14 +- .../chat/browser/terminalChatActions.ts | 62 +- .../chat/browser/terminalChatController.ts | 336 ++-- .../chat/browser/terminalChatEnabler.ts | 35 + .../chat/browser/terminalChatWidget.ts | 59 +- .../terminalInitialHintConfiguration.ts | 3 +- .../terminal.developer.contribution.ts | 2 +- .../links/browser/terminalLinkOpeners.ts | 8 +- .../test/browser/terminalLinkHelpers.test.ts | 2 +- .../browser/media/stickyScroll.css | 1 + .../terminalStickyScrollColorRegistry.ts | 19 +- .../suggest/browser/terminalSuggestAddon.ts | 26 +- .../common/terminalSuggestConfiguration.ts | 8 - .../windows11_pwsh_input_ls_complete_ls.ts | 4 - .../windows11_pwsh_namespace_completion.ts | 18 +- .../test/browser/terminalTypeAhead.test.ts | 2 +- .../browser/codeCoverageDecorations.ts | 145 +- .../explorerProjections/treeProjection.ts | 6 +- .../testing/browser/testCoverageBars.ts | 12 +- .../testing/browser/testCoverageView.ts | 2 +- .../testing/browser/testExplorerActions.ts | 74 +- .../testing/browser/testingDecorations.ts | 2 +- .../testing/browser/testingExplorerFilter.ts | 3 +- .../testing/browser/testingExplorerView.ts | 58 +- .../testing/browser/testingOutputPeek.ts | 33 +- .../contrib/testing/browser/theme.ts | 104 +- .../contrib/testing/common/configuration.ts | 2 +- .../contrib/testing/common/testCoverage.ts | 130 +- .../testing/common/testCoverageService.ts | 15 +- .../testing/common/testExplorerFilterState.ts | 2 + .../testing/common/testProfileService.ts | 22 +- .../testing/common/testResultService.ts | 4 +- .../contrib/testing/common/testService.ts | 7 +- .../contrib/testing/common/testServiceImpl.ts | 43 +- .../contrib/testing/common/testTypes.ts | 11 +- .../testing/common/testingContextKeys.ts | 1 + .../common/testingContinuousRunService.ts | 2 +- .../nameProjection.test.ts | 3 +- .../treeProjection.test.ts | 51 +- .../testing/test/common/testCoverage.test.ts | 58 +- .../common/testExplorerFilterState.test.ts | 2 +- .../test/common/testProfileService.test.ts | 2 +- .../test/common/testResultService.test.ts | 4 +- .../test/common/testResultStorage.test.ts | 5 +- .../testing/test/common/testingUri.test.ts | 2 +- .../test/node/colorRegistry.releaseTest.ts | 2 +- .../contrib/timeline/browser/timelinePane.ts | 9 + .../update/browser/releaseNotesEditor.ts | 6 +- .../url/test/browser/trustedDomains.test.ts | 2 +- .../browser/media/userDataProfilesEditor.css | 187 ++- .../browser/userDataProfile.ts | 220 ++- .../browser/userDataProfilesEditor.ts | 618 ++++--- .../browser/userDataProfilesEditorModel.ts | 707 ++++++-- .../webview/browser/pre/index-no-csp.html | 6 +- .../contrib/webview/browser/pre/index.html | 32 +- .../webview/browser/pre/service-worker.js | 3 +- .../contrib/webview/browser/themeing.ts | 4 +- .../contrib/webview/browser/webviewElement.ts | 17 +- .../webview/browser/webviewMessages.d.ts | 5 + .../browser/webviewWindowDragMonitor.ts | 26 +- .../browser/webviewIconManager.ts | 19 +- .../browser/gettingStarted.ts | 115 +- .../browser/gettingStartedColors.ts | 6 +- .../browser/gettingStartedDetailsRenderer.ts | 2 +- .../browser/gettingStartedList.ts | 2 +- .../browser/media/gettingStarted.css | 11 +- .../gettingStartedMarkdownRenderer.test.ts | 2 +- .../browser/media/walkThroughPart.css | 4 +- .../browser/walkThroughPart.ts | 4 +- .../electron-sandbox/desktop.contribution.ts | 10 +- src/vs/workbench/electron-sandbox/window.ts | 26 +- .../actions/common/menusExtensionPoint.ts | 2 +- .../aiRelatedInformationService.test.ts | 2 +- .../browser/authenticationService.ts | 8 +- .../authentication/common/authentication.ts | 21 +- .../browser/authenticationService.test.ts | 2 +- .../test/common/commandService.test.ts | 2 +- .../test/browser/configuration.test.ts | 2 +- .../test/browser/configurationEditing.test.ts | 2 +- .../test/browser/configurationService.test.ts | 2 +- .../test/common/configurationModels.test.ts | 2 +- .../baseConfigurationResolverService.ts | 2 +- .../configurationResolverService.test.ts | 2 +- .../decorations/browser/decorationsService.ts | 10 +- .../test/browser/decorationsService.test.ts | 2 +- .../dialogs/browser/simpleFileDialog.ts | 2 +- .../fileDialogService.test.ts | 8 +- .../editor/common/customEditorLabelService.ts | 80 +- .../browser/customEditorLabelService.test.ts | 234 +++ .../test/browser/editorGroupsService.test.ts | 2 +- .../browser/editorResolverService.test.ts | 2 +- .../editor/test/browser/editorService.test.ts | 2 +- .../test/browser/editorsObserver.test.ts | 2 +- .../browser/webExtensionsScannerService.ts | 15 +- .../common/extensionManagement.ts | 1 + .../extensionManagementChannelClient.ts | 16 +- .../common/extensionManagementService.ts | 28 +- .../remoteExtensionManagementService.ts | 9 +- .../common/webExtensionManagementService.ts | 6 +- .../nativeExtensionManagementService.ts | 4 +- .../remoteExtensionManagementService.ts | 5 + .../extensionEnablementService.test.ts | 8 +- .../extensions/browser/extensionService.ts | 15 +- .../common/abstractExtensionService.ts | 159 +- .../services/extensions/common/extensions.ts | 14 +- .../common/extensionsApiProposals.ts | 130 -- .../common/extensionsProposedApi.ts | 50 +- .../extensions/common/extensionsRegistry.ts | 18 +- .../extensions/common/extensionsUtil.ts | 7 +- .../services/extensions/common/rpcProtocol.ts | 2 +- .../cachedExtensionScanner.ts | 9 +- .../localProcessExtensionHost.ts | 2 +- .../nativeExtensionService.ts | 10 +- .../test/browser/extensionService.test.ts | 6 +- .../browser/extensionStorageMigration.test.ts | 2 +- .../extensionDescriptionRegistry.test.ts | 5 +- ...extensionManifestPropertiesService.test.ts | 2 +- .../test/common/rpcProtocol.test.ts | 2 +- .../common/filesConfigurationService.ts | 2 +- .../test/browser/historyService.test.ts | 2 +- .../host/browser/browserHostService.ts | 8 + .../workbench/services/host/browser/host.ts | 8 +- .../electron-sandbox/nativeHostService.ts | 12 +- .../browser/browserKeyboardMapper.test.ts | 2 +- .../test/browser/keybindingEditing.test.ts | 2 +- .../test/browser/keybindingIO.test.ts | 2 +- .../test/node/keyboardMapperTestUtils.ts | 2 +- .../test/node/macLinuxKeyboardMapper.test.ts | 2 +- .../services/label/test/browser/label.test.ts | 2 +- .../language/common/languageService.ts | 9 + .../test/common/languageRuntime.test.ts | 2 +- .../services/layout/browser/layoutService.ts | 2 +- .../electron-sandbox/lifecycleService.test.ts | 2 +- .../electron-sandbox/localeService.ts | 4 +- .../preferences/common/preferences.ts | 4 +- .../preferences/common/preferencesModels.ts | 4 +- .../browser/keybindingsEditorModel.test.ts | 2 +- .../test/browser/preferencesService.test.ts | 2 +- .../test/common/preferencesValidation.test.ts | 2 +- .../progress/browser/progressService.ts | 15 +- .../test/browser/progressIndicator.test.ts | 2 +- .../remote/common/remoteExtensionsScanner.ts | 24 +- .../services/search/common/search.ts | 1 + .../search/common/searchExtTypesInternal.ts | 13 + .../services/search/common/searchService.ts | 4 +- .../search/common/textSearchManager.ts | 4 +- .../services/search/node/fileSearch.ts | 14 +- .../services/search/node/rawSearchService.ts | 26 +- .../services/search/node/ripgrepFileSearch.ts | 10 +- .../search/node/ripgrepSearchProvider.ts | 18 +- .../search/node/ripgrepTextSearchEngine.ts | 15 +- .../services/search/node/textSearchAdapter.ts | 4 +- .../search/test/browser/queryBuilder.test.ts | 2 +- .../search/test/common/ignoreFile.test.ts | 2 +- .../search/test/common/queryBuilder.test.ts | 2 +- .../search/test/common/replace.test.ts | 2 +- .../search/test/common/search.test.ts | 2 +- .../search/test/common/searchHelpers.test.ts | 2 +- .../test/node/fileSearch.integrationTest.ts | 6 +- .../node/rawSearchService.integrationTest.ts | 2 +- .../test/node/ripgrepFileSearch.test.ts | 2 +- .../node/ripgrepTextSearchEngineUtils.test.ts | 2 +- .../test/node/search.integrationTest.ts | 2 +- .../test/node/textSearch.integrationTest.ts | 2 +- .../test/node/textSearchManager.test.ts | 2 +- .../services/statusbar/browser/statusbar.ts | 4 +- .../suggest/browser/media/suggest.css | 3 +- .../suggest/browser/simpleCompletionItem.ts | 2 +- .../suggest/browser/simpleCompletionModel.ts | 4 +- .../test/browser/commonProperties.test.ts | 2 +- .../test/node/commonProperties.test.ts | 2 +- .../worker/textMateWorkerTokenizer.ts | 2 + .../textMateTokenizationFeatureImpl.ts | 12 +- .../tokenizationSupportWithLineLimit.ts | 4 +- .../services/textMate/common/TMGrammars.ts | 2 +- .../test/browser/arrayOperation.test.ts | 2 +- .../textfile/browser/textFileService.ts | 7 +- .../services/textfile/common/encoding.ts | 109 +- .../textfile/common/textFileEditorModel.ts | 24 +- .../common/textFileEditorModelManager.ts | 5 +- .../common/textFileSaveParticipant.ts | 50 +- .../services/textfile/common/textfiles.ts | 7 +- .../test/browser/textEditorService.test.ts | 2 +- .../textFileEditorModel.integrationTest.ts | 2 +- .../test/browser/textFileEditorModel.test.ts | 2 +- .../textFileEditorModelManager.test.ts | 2 +- .../test/browser/textFileService.test.ts | 2 +- .../textfile/test/common/fixtures/files.ts | 4 +- .../test/common/textFileService.io.test.ts | 17 +- .../nativeTextFileService.test.ts | 2 +- .../node/encoding/encoding.integrationTest.ts | 2 +- .../test/node/encoding/encoding.test.ts | 30 +- .../encoding/fixtures/some.shiftjis.1.txt | 2 + .../browser/textModelResolverService.test.ts | 2 +- .../themes/browser/fileIconThemeData.ts | 2 +- .../themes/browser/workbenchThemeService.ts | 2 + .../services/themes/common/colorThemeData.ts | 43 +- .../test/node/tokenStyleResolving.test.ts | 2 +- .../tunnel/electron-sandbox/tunnelService.ts | 4 +- .../untitledTextEditor.integrationTest.ts | 2 +- .../test/browser/untitledTextEditor.test.ts | 2 +- .../test/browser/domActivityTracker.test.ts | 2 +- .../browser/extensionsResource.ts | 16 +- .../userDataProfileImportExportService.ts | 152 +- .../browser/userDataProfileManagement.ts | 9 +- .../browser/userDataProfileStorageService.ts | 2 +- .../userDataProfile/common/userDataProfile.ts | 23 +- .../common/userDataProfileIcons.ts | 2 +- .../services/views/browser/viewsService.ts | 20 +- .../test/browser/viewContainerModel.test.ts | 2 +- .../browser/viewDescriptorService.test.ts | 2 +- .../common/fileWorkingCopyManager.ts | 6 +- .../common/storedFileWorkingCopy.ts | 24 +- .../common/storedFileWorkingCopyManager.ts | 6 +- .../storedFileWorkingCopySaveParticipant.ts | 51 +- .../common/workingCopyFileService.ts | 6 +- .../browser/fileWorkingCopyManager.test.ts | 4 +- .../test/browser/resourceWorkingCopy.test.ts | 2 +- .../browser/storedFileWorkingCopy.test.ts | 6 +- .../storedFileWorkingCopyManager.test.ts | 5 +- .../browser/untitledFileWorkingCopy.test.ts | 2 +- .../untitledFileWorkingCopyManager.test.ts | 8 +- .../untitledScratchpadWorkingCopy.test.ts | 2 +- .../browser/workingCopyBackupTracker.test.ts | 2 +- .../browser/workingCopyEditorService.test.ts | 2 +- .../browser/workingCopyFileService.test.ts | 2 +- .../test/common/workingCopyService.test.ts | 2 +- .../workingCopyBackupService.test.ts | 2 +- .../workingCopyBackupTracker.test.ts | 2 +- .../workingCopyHistoryService.test.ts | 2 +- .../workingCopyHistoryTracker.test.ts | 2 +- .../abstractWorkspaceEditingService.ts | 7 +- .../workspaceEditingService.ts | 4 +- .../test/browser/workspaces.test.ts | 2 +- .../test/common/workspaceTrust.test.ts | 2 +- .../workbench/test/browser/codeeditor.test.ts | 2 +- .../test/browser/contributions.test.ts | 2 +- src/vs/workbench/test/browser/part.test.ts | 4 +- .../parts/editor/breadcrumbModel.test.ts | 2 +- .../parts/editor/diffEditorInput.test.ts | 2 +- .../test/browser/parts/editor/editor.test.ts | 2 +- .../parts/editor/editorDiffModel.test.ts | 2 +- .../parts/editor/editorGroupModel.test.ts | 2 +- .../browser/parts/editor/editorInput.test.ts | 2 +- .../browser/parts/editor/editorModel.test.ts | 2 +- .../browser/parts/editor/editorPane.test.ts | 2 +- .../editor/filteredEditorGroupModel.test.ts | 2 +- .../parts/editor/resourceEditorInput.test.ts | 2 +- .../editor/sideBySideEditorInput.test.ts | 2 +- .../parts/editor/textEditorPane.test.ts | 2 +- .../editor/textResourceEditorInput.test.ts | 2 +- .../parts/statusbar/statusbarModel.test.ts | 2 +- .../test/browser/quickAccess.test.ts | 2 +- src/vs/workbench/test/browser/viewlet.test.ts | 2 +- src/vs/workbench/test/browser/webview.test.ts | 2 +- src/vs/workbench/test/browser/window.test.ts | 2 +- .../test/browser/workbenchTestServices.ts | 8 +- src/vs/workbench/test/common/memento.test.ts | 2 +- .../test/common/notifications.test.ts | 2 +- .../workbench/test/common/resources.test.ts | 2 +- src/vs/workbench/test/common/utils.ts | 2 +- .../test/common/workbenchTestServices.ts | 3 +- .../electron-sandbox/resolveExternal.test.ts | 2 +- src/vs/workbench/workbench.common.main.ts | 3 + src/vs/workbench/workbench.desktop.main.ts | 3 + src/vs/workbench/workbench.web.main.ts | 1 + src/vscode-dts/README.md | 2 +- src/vscode-dts/vscode.d.ts | 51 +- .../vscode.proposed.aiTextSearchProvider.d.ts | 196 --- .../vscode.proposed.attributableCoverage.d.ts | 38 +- .../vscode.proposed.authGetSessions.d.ts | 52 +- .../vscode.proposed.chatProvider.d.ts | 25 +- .../vscode.proposed.chatVariableResolver.d.ts | 9 - .../vscode.proposed.commentReveal.d.ts | 32 + ...e.proposed.commentThreadApplicability.d.ts | 10 + ...code.proposed.createFileSystemWatcher.d.ts | 2 +- .../vscode.proposed.fileComments.d.ts | 65 +- .../vscode.proposed.fileSearchProvider.d.ts | 6 + ...e.proposed.inlineCompletionsAdditions.d.ts | 6 + src/vscode-dts/vscode.proposed.lmTools.d.ts | 91 + .../vscode.proposed.mappedEditsProvider.d.ts | 2 +- .../vscode.proposed.scmHistoryProvider.d.ts | 16 +- ...ode.proposed.terminalShellIntegration.d.ts | 51 +- test/automation/src/search.ts | 15 + test/automation/src/terminal.ts | 1 + test/integration/browser/src/index.ts | 4 + test/smoke/src/areas/search/search.test.ts | 11 + test/unit/electron/renderer.js | 8 +- test/unit/node/index.js | 2 +- yarn.lock | 174 +- 1577 files changed, 26753 insertions(+), 14508 deletions(-) create mode 100644 .github/workflows/on-reopen.yml create mode 100644 extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts create mode 100644 extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts create mode 100644 src/vs/base/browser/domObservable.ts rename src/vs/base/common/{stripComments.d.ts => jsonc.d.ts} (74%) rename src/vs/base/common/{stripComments.js => jsonc.js} (78%) create mode 100644 src/vs/base/common/observableInternal/api.ts create mode 100644 src/vs/base/common/observableInternal/lazyObservableValue.ts rename src/vs/base/test/common/{stripComments.test.ts => jsonParse.test.ts} (57%) create mode 100644 src/vs/base/test/common/numbers.test.ts create mode 100644 src/vs/editor/browser/observableCodeEditor.ts delete mode 100644 src/vs/editor/browser/observableUtilities.ts create mode 100644 src/vs/editor/common/cursor/cursorTypeEditOperations.ts create mode 100644 src/vs/editor/contrib/hover/browser/contentHoverRendered.ts create mode 100644 src/vs/editor/contrib/inlineEdits/browser/commands.ts create mode 100644 src/vs/editor/contrib/inlineEdits/browser/consts.ts create mode 100644 src/vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution.ts create mode 100644 src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts create mode 100644 src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts create mode 100644 src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.css create mode 100644 src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts rename src/vs/editor/contrib/placeholderText/browser/{placeholderTextContribution.ts => placeholderText.contribution.ts} (70%) create mode 100644 src/vs/editor/test/browser/widget/observableCodeEditor.test.ts create mode 100644 src/vs/editor/test/node/diffing/fixtures/issue-214049/1.txt create mode 100644 src/vs/editor/test/node/diffing/fixtures/issue-214049/2.txt create mode 100644 src/vs/editor/test/node/diffing/fixtures/issue-214049/advanced.expected.diff.json create mode 100644 src/vs/editor/test/node/diffing/fixtures/issue-214049/legacy.expected.diff.json create mode 100644 src/vs/platform/extensions/common/extensionsApiProposals.ts create mode 100644 src/vs/platform/extensions/test/common/extensions.test.ts create mode 100644 src/vs/platform/issue/electron-main/processMainService.ts create mode 100644 src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts create mode 100644 src/vs/workbench/api/common/extHostLanguageModelTools.ts create mode 100644 src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollections.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts rename src/vs/workbench/contrib/chat/browser/{ => chatContentParts}/chatConfirmationWidget.ts (100%) create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/chatWarningContentPart.ts rename src/vs/workbench/contrib/chat/browser/{ => chatContentParts}/media/chatConfirmationWidget.css (100%) create mode 100644 src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts create mode 100644 src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts create mode 100644 src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css create mode 100644 src/vs/workbench/contrib/chat/common/languageModelToolsService.ts create mode 100644 src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap create mode 100644 src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap create mode 100644 src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts create mode 100644 src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts delete mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget.ts create mode 100644 src/vs/workbench/contrib/interactive/browser/replInputHintContentWidget.ts create mode 100644 src/vs/workbench/contrib/issue/electron-sandbox/process.contribution.ts create mode 100644 src/vs/workbench/contrib/issue/electron-sandbox/processService.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory.ts create mode 100644 src/vs/workbench/contrib/replNotebook/browser/interactiveEditor.css create mode 100644 src/vs/workbench/contrib/replNotebook/browser/media/interactive.css create mode 100644 src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts create mode 100644 src/vs/workbench/contrib/replNotebook/browser/replEditor.ts create mode 100644 src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts create mode 100644 src/vs/workbench/contrib/scm/browser/scmHistory.ts create mode 100644 src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts create mode 100644 src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts create mode 100644 src/vs/workbench/services/editor/test/browser/customEditorLabelService.test.ts delete mode 100644 src/vs/workbench/services/extensions/common/extensionsApiProposals.ts create mode 100644 src/vs/workbench/services/search/common/searchExtTypesInternal.ts create mode 100644 src/vs/workbench/services/textfile/test/node/encoding/fixtures/some.shiftjis.1.txt create mode 100644 src/vscode-dts/vscode.proposed.commentReveal.d.ts create mode 100644 src/vscode-dts/vscode.proposed.lmTools.d.ts diff --git a/.configurations/configuration.dsc.yaml b/.configurations/configuration.dsc.yaml index 255da69a5bb..780b1dfa959 100644 --- a/.configurations/configuration.dsc.yaml +++ b/.configurations/configuration.dsc.yaml @@ -12,11 +12,11 @@ properties: - resource: Microsoft.WinGet.DSC/WinGetPackage id: npm directives: - description: Install NodeJS version >=18.15.x and <19 + description: Install NodeJS version 20 allowPrerelease: true settings: id: OpenJS.NodeJS.LTS - version: "18.18.0" + version: "20.14.0" source: winget - resource: NpmDsc/NpmPackage id: yarn diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 31d67db5fac..bc30d7dbe3b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/typescript-node:18-bookworm +FROM mcr.microsoft.com/devcontainers/typescript-node:20-bookworm ADD install-vscode.sh /root/ RUN /root/install-vscode.sh diff --git a/.eslintrc.json b/.eslintrc.json index 2b80487b083..b981428cb72 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -668,7 +668,6 @@ "events", "fs", "fs/promises", - "graceful-fs", "http", "https", "minimist", @@ -690,6 +689,7 @@ "vscode-regexpp", "vscode-textmate", "worker_threads", + "@xterm/addon-clipboard", "@xterm/addon-image", "@xterm/addon-search", "@xterm/addon-serialize", diff --git a/.github/classifier.json b/.github/classifier.json index 44514039e1e..d0a2c778997 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -32,7 +32,7 @@ "debug": {"assign": ["roblourens"]}, "debug-disassembly": {"assign": []}, "dialogs": {"assign": ["sbatten"]}, - "diff-editor": {"assign": ["alexdima"]}, + "diff-editor": {"assign": ["hediet"]}, "dropdown": {"assign": ["lramos15"]}, "editor-api": {"assign": ["alexdima"]}, "editor-autoclosing": {"assign": ["alexdima"]}, @@ -116,7 +116,7 @@ "json": {"assign": ["aeschli"]}, "json-sorting": {"assign": ["aiday-mar"]}, "keybindings": {"assign": ["ulugbekna"]}, - "keybindings-editor": {"assign": ["sandy081"]}, + "keybindings-editor": {"assign": ["ulugbekna"]}, "keyboard-layout": {"assign": ["ulugbekna"]}, "L10N": {"assign": ["TylerLeonhardt", "csigs"]}, "l10n-platform": {"assign": ["TylerLeonhardt"]}, diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml index 5860349a437..ef775ce8fdf 100644 --- a/.github/workflows/locker.yml +++ b/.github/workflows/locker.yml @@ -20,9 +20,10 @@ jobs: - name: Run Locker uses: ./actions/locker with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} daysSinceClose: 45 - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} daysSinceUpdate: 3 ignoredLabel: "*out-of-scope,accessibility" ignoreLabelUntil: "author-verification-requested" + ignoredMilestones: "Backlog Candidates" labelUntil: "verified" diff --git a/.github/workflows/on-open.yml b/.github/workflows/on-open.yml index 361ac11b946..2a26794c6b0 100644 --- a/.github/workflows/on-open.yml +++ b/.github/workflows/on-open.yml @@ -16,7 +16,13 @@ jobs: - name: Install Actions run: npm install --production --prefix ./actions + - name: Check for Validity + uses: ./actions/validity-checker + with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + - name: Run CopyCat (VSCodeTriageBot/testissues) + if: github.event.issue.user.login != 'ghost' uses: ./actions/copycat with: appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} @@ -25,6 +31,7 @@ jobs: repo: testissues - name: Run New Release + if: github.event.issue.user.login != 'ghost' uses: ./actions/new-release with: label: new release @@ -36,6 +43,7 @@ jobs: days: 5 - name: Run Clipboard Labeler + if: github.event.issue.user.login != 'ghost' uses: ./actions/regex-labeler with: appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} @@ -44,6 +52,7 @@ jobs: comment: "It looks like you're using the VS Code Issue Reporter but did not paste the text generated into the created issue. We've closed this issue, please open a new one containing the text we placed in your clipboard.\n\nHappy Coding!" - name: Run Clipboard Labeler (Chinese) + if: github.event.issue.user.login != 'ghost' uses: ./actions/regex-labeler with: appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} @@ -53,6 +62,7 @@ jobs: # source of truth in ./english-please.yml - name: Run English Please + if: github.event.issue.user.login != 'ghost' uses: ./actions/english-please with: token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} @@ -64,6 +74,7 @@ jobs: translatorRequestedLabelColor: "c29cff" # source of truth in ./test-plan-item-validator.yml - name: Run Test Plan Item Validator + if: github.event.issue.user.login != 'ghost' uses: ./actions/test-plan-item-validator with: token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.github/workflows/on-reopen.yml b/.github/workflows/on-reopen.yml new file mode 100644 index 00000000000..d29de326c53 --- /dev/null +++ b/.github/workflows/on-reopen.yml @@ -0,0 +1,22 @@ +name: On Reopen +on: + issues: + types: [reopened] + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v4 + with: + repository: "microsoft/vscode-github-triage-actions" + ref: stable + path: ./actions + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: Check for Validity + uses: ./actions/validity-checker + with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts index 7280782c10a..3fff7c5b637 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts @@ -5,7 +5,7 @@ import { IstanbulCoverageContext } from 'istanbul-to-vscode'; import * as vscode from 'vscode'; -import { SourceLocationMapper, SourceMapStore } from './testOutputScanner'; +import { SearchStrategy, SourceLocationMapper, SourceMapStore } from './testOutputScanner'; import { IScriptCoverage, OffsetToPosition, RangeCoverageTracker } from './v8CoverageWrangling'; export const istanbulCoverageContext = new IstanbulCoverageContext(); @@ -18,7 +18,7 @@ export const istanbulCoverageContext = new IstanbulCoverageContext(); export class PerTestCoverageTracker { private readonly scripts = new Map(); - constructor(private readonly maps: SourceMapStore) {} + constructor(private readonly maps: SourceMapStore) { } public add(coverage: IScriptCoverage, test?: vscode.TestItem) { const script = this.scripts.get(coverage.scriptId); @@ -71,11 +71,7 @@ class Script { public async report(run: vscode.TestRun) { const mapper = await this.maps.getSourceLocationMapper(this.uri.toString()); const originalUri = (await this.maps.getSourceFile(this.uri.toString())) || this.uri; - - run.addCoverage(this.overall.report(originalUri, this.converter, mapper)); - for (const [test, projection] of this.perItem) { - run.addCoverage(projection.report(originalUri, this.converter, mapper, test)); - } + run.addCoverage(this.overall.report(originalUri, this.converter, mapper, this.perItem)); } } @@ -88,20 +84,11 @@ class ScriptCoverageTracker { } } - /** - * Generates the script's coverage for the test run. - * - * If a source location mapper is given, it assumes the `uri` is the mapped - * URI, and that any unmapped locations/outside the URI should be ignored. - */ - public report( + public *toDetails( uri: vscode.Uri, convert: OffsetToPosition, mapper: SourceLocationMapper | undefined, - item?: vscode.TestItem - ): V8CoverageFile { - const file = new V8CoverageFile(uri, item); - + ) { for (const range of this.coverage) { if (range.start === range.end) { continue; @@ -113,8 +100,8 @@ class ScriptCoverageTracker { const endCov = convert.toLineColumn(range.end); let end = new vscode.Position(endCov.line, endCov.column); if (mapper) { - const startMap = mapper(start.line, start.character); - const endMap = startMap && mapper(end.line, end.character); + const startMap = mapper(start.line, start.character, SearchStrategy.FirstAfter); + const endMap = startMap && mapper(end.line, end.character, SearchStrategy.FirstBefore); if (!endMap || uri.toString().toLowerCase() !== endMap.uri.toString().toLowerCase()) { continue; } @@ -123,28 +110,48 @@ class ScriptCoverageTracker { } for (let i = start.line; i <= end.line; i++) { - file.add( - new vscode.StatementCoverage( - range.covered, - new vscode.Range( - new vscode.Position(i, i === start.line ? start.character : 0), - new vscode.Position(i, i === end.line ? end.character : Number.MAX_SAFE_INTEGER) - ) + yield new vscode.StatementCoverage( + range.covered, + new vscode.Range( + new vscode.Position(i, i === start.line ? start.character : 0), + new vscode.Position(i, i === end.line ? end.character : Number.MAX_SAFE_INTEGER) ) ); } } + } + + /** + * Generates the script's coverage for the test run. + * + * If a source location mapper is given, it assumes the `uri` is the mapped + * URI, and that any unmapped locations/outside the URI should be ignored. + */ + public report( + uri: vscode.Uri, + convert: OffsetToPosition, + mapper: SourceLocationMapper | undefined, + items: Map, + ): V8CoverageFile { + const file = new V8CoverageFile(uri, items, convert, mapper); + for (const detail of this.toDetails(uri, convert, mapper)) { + file.add(detail); + } return file; } } -export class V8CoverageFile extends vscode.FileCoverage { +export class V8CoverageFile extends vscode.FileCoverage2 { public details: vscode.StatementCoverage[] = []; - constructor(uri: vscode.Uri, item?: vscode.TestItem) { - super(uri, { covered: 0, total: 0 }); - (this as vscode.FileCoverage2).testItem = item; + constructor( + uri: vscode.Uri, + private readonly perTest: Map, + private readonly convert: OffsetToPosition, + private readonly mapper: SourceLocationMapper | undefined, + ) { + super(uri, { covered: 0, total: 0 }, undefined, undefined, [...perTest.keys()]); } public add(detail: vscode.StatementCoverage) { @@ -154,4 +161,9 @@ export class V8CoverageFile extends vscode.FileCoverage { this.statementCoverage.covered++; } } + + public testDetails(test: vscode.TestItem): vscode.FileCoverageDetail[] { + const t = this.perTest.get(test); + return t ? [...t.toDetails(this.uri, this.convert, this.mapper)] : []; + } } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts index 960dbcf634e..491f67ee300 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -44,7 +44,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.tests.registerTestFollowupProvider({ async provideFollowup(_result, test, taskIndex, messageIndex, _token) { return [{ - title: '$(sparkle) Ask copilot for help', + title: '$(sparkle) Fix with Copilot', command: 'github.copilot.tests.fixTestFailure', arguments: [{ source: 'peekFollowup', test, message: test.taskStates[taskIndex].messages[messageIndex] }] }]; @@ -119,7 +119,7 @@ export async function activate(context: vscode.ExtensionContext) { map, task, kind === vscode.TestRunProfileKind.Debug - ? await runner.debug(currentArgs, req.include) + ? await runner.debug(task, currentArgs, req.include) : await runner.run(currentArgs, req.include), coverageDir, cancellationToken @@ -196,13 +196,8 @@ export async function activate(context: vscode.ExtensionContext) { true ); - coverage.loadDetailedCoverage = async (_run, coverage) => { - if (coverage instanceof V8CoverageFile) { - return coverage.details; - } - - return []; - }; + coverage.loadDetailedCoverage = async (_run, coverage) => coverage instanceof V8CoverageFile ? coverage.details : []; + coverage.loadDetailedCoverageForTest = async (_run, coverage, test) => coverage instanceof V8CoverageFile ? coverage.testDetails(test) : []; for (const [name, arg] of browserArgs) { const cfg = ctrl.createRunProfile( diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts index 74013dcd561..b5a448aaba1 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -424,36 +424,62 @@ const tryMakeMarkdown = (message: string) => { const inlineSourcemapRe = /^\/\/# sourceMappingURL=data:application\/json;base64,(.+)/m; const sourceMapBiases = [GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND] as const; -export type SourceLocationMapper = (line: number, col: number) => vscode.Location | undefined; +export const enum SearchStrategy { + FirstBefore = -1, + FirstAfter = 1, +} + +export type SourceLocationMapper = (line: number, col: number, strategy: SearchStrategy) => vscode.Location | undefined; export class SourceMapStore { private readonly cache = new Map>(); - async getSourceLocationMapper(fileUri: string) { + async getSourceLocationMapper(fileUri: string): Promise { const sourceMap = await this.loadSourceMap(fileUri); - return (line: number, col: number) => { + return (line, col, strategy) => { if (!sourceMap) { return undefined; } - let smLine = line + 1; + // 1. Look for the ideal position on this line if it exists + const idealPosition = originalPositionFor(sourceMap, { column: col, line: line + 1, bias: SearchStrategy.FirstAfter ? GREATEST_LOWER_BOUND : LEAST_UPPER_BOUND }); + if (idealPosition.line !== null && idealPosition.column !== null && idealPosition.source !== null) { + return new vscode.Location( + this.completeSourceMapUrl(sourceMap, idealPosition.source), + new vscode.Position(idealPosition.line - 1, idealPosition.column) + ); + } - // if the range is after the end of mappings, adjust it to the last mapped line + // Otherwise get the first/last valid mapping on another line. const decoded = decodedMappings(sourceMap); - if (decoded.length <= line) { - smLine = decoded.length; // base 1, no -1 needed - col = Number.MAX_SAFE_INTEGER; + const enum MapField { + COLUMN = 0, + SOURCES_INDEX = 1, + SOURCE_LINE = 2, + SOURCE_COLUMN = 3, } - for (const bias of sourceMapBiases) { - const position = originalPositionFor(sourceMap, { column: col, line: smLine, bias }); - if (position.line !== null && position.column !== null && position.source !== null) { - return new vscode.Location( - this.completeSourceMapUrl(sourceMap, position.source), - new vscode.Position(position.line - 1, position.column) - ); + do { + line += strategy; + const segments = decoded[line]; + if (!segments?.length) { + continue; } - } + + const index = strategy === SearchStrategy.FirstBefore + ? findLastIndex(segments, s => s.length !== 1) + : segments.findIndex(s => s.length !== 1); + const segment = segments[index]; + + if (!segment || segment.length === 1) { + continue; + } + + return new vscode.Location( + this.completeSourceMapUrl(sourceMap, sourceMap.sources[segment[MapField.SOURCES_INDEX]]!), + new vscode.Position(segment[MapField.SOURCE_LINE] - 1, segment[MapField.SOURCE_COLUMN]) + ); + } while (strategy === SearchStrategy.FirstBefore ? line > 0 : line < decoded.length); return undefined; }; @@ -461,7 +487,31 @@ export class SourceMapStore { /** Gets an original location from a base 0 line and column */ async getSourceLocation(fileUri: string, line: number, col = 0) { - return this.getSourceLocationMapper(fileUri).then(m => m(line, col)); + const sourceMap = await this.loadSourceMap(fileUri); + if (!sourceMap) { + return undefined; + } + + let smLine = line + 1; + + // if the range is after the end of mappings, adjust it to the last mapped line + const decoded = decodedMappings(sourceMap); + if (decoded.length <= line) { + smLine = decoded.length; // base 1, no -1 needed + col = Number.MAX_SAFE_INTEGER; + } + + for (const bias of sourceMapBiases) { + const position = originalPositionFor(sourceMap, { column: col, line: smLine, bias }); + if (position.line !== null && position.column !== null && position.source !== null) { + return new vscode.Location( + this.completeSourceMapUrl(sourceMap, position.source), + new vscode.Position(position.line - 1, position.column) + ); + } + } + + return undefined; } async getSourceFile(compiledUri: string) { @@ -602,3 +652,13 @@ async function deriveSourceLocation(store: SourceMapStore, parts: RegExpMatchArr const [, fileUri, line, col] = parts; return store.getSourceLocation(fileUri, Number(line) - 1, Number(col)); } + +function findLastIndex(arr: T[], predicate: (value: T) => boolean) { + for (let i = arr.length - 1; i >= 0; i--) { + if (predicate(arr[i])) { + return i; + } + } + + return -1; +} \ No newline at end of file diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts index 8a76cefe36a..954b847f4a8 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts @@ -37,7 +37,7 @@ export abstract class VSCodeTestRunner { return new TestOutputScanner(cp, args); } - public async debug(baseArgs: ReadonlyArray, filter?: ReadonlyArray) { + public async debug(testRun: vscode.TestRun, baseArgs: ReadonlyArray, filter?: ReadonlyArray) { const port = await this.findOpenPort(); const baseConfiguration = vscode.workspace .getConfiguration('launch', this.repoLocation) @@ -95,7 +95,7 @@ export abstract class VSCodeTestRunner { }, }); - vscode.debug.startDebugging(this.repoLocation, { ...baseConfiguration, port }); + vscode.debug.startDebugging(this.repoLocation, { ...baseConfiguration, port }, { testRun }); let exited = false; let rootSession: vscode.DebugSession | undefined; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json index 0183a2ff57e..9725e14041e 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json @@ -11,6 +11,6 @@ "src/**/*", "../../../src/vscode-dts/vscode.d.ts", "../../../src/vscode-dts/vscode.proposed.testObserver.d.ts", - "../../../src/vscode-dts/vscode.proposed.attributableCoverage.d.ts", + "../../../src/vscode-dts/vscode.proposed.attributableCoverage.d.ts" ] } diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 2ac7d699582..957ce5a9ee0 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"May 2024\"" + "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"June 2024\"" }, { "kind": 1, diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index d7836922bad..e1c0a9fce02 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:\"May 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\r\n\r\n$MILESTONE=milestone:\"June 2024\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 3bf56fffce4..0b260270ed7 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:\"May 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:\"June 2024\"\n\n$MINE=assignee:@me" }, { "kind": 1, @@ -157,7 +157,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE -$MINE is:issue is:closed reason:completed sort:updated-asc label:bug -label:unreleased -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:*out-of-scope -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:andreamah -author:bamurtaugh -author:bpasero -author:chrisdias -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:gregvanl -author:hediet -author:isidorn -author:joaomoreno -author:joyceerhl -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:tanhakabir -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:paulacamargo25 -author:ulugbekna -author:aiday-mar -author:daviddossett -author:bhavyaus -author:justschen -author:benibenj -author:luabud" + "value": "$REPOS $MILESTONE -$MINE is:issue is:closed reason:completed sort:updated-asc label:bug -label:unreleased -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:*out-of-scope -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:andreamah -author:bamurtaugh -author:bpasero -author:chrisdias -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:gregvanl -author:hediet -author:isidorn -author:joaomoreno -author:joyceerhl -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:tanhakabir -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:paulacamargo25 -author:ulugbekna -author:aiday-mar -author:daviddossett -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1" }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index 091c6e8a886..27fc3c2ecb5 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$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// current milestone name\n$MILESTONE=milestone:\"May 2024\"\n" + "value": "// list of repos we work in\n$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// current milestone name\n$MILESTONE=milestone:\"June 2024\"\n" }, { "kind": 1, @@ -102,7 +102,7 @@ { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode assignee:@me is:open type:issue -label:\"info-needed\" -label:api -label:api-finalization -label:api-proposal -label:authentication -label:bisect-ext -label:bracket-pair-colorization -label:bracket-pair-guides -label:breadcrumbs -label:callhierarchy -label:chrome-devtools -label:code-lens -label:command-center -label:comments -label:config -label:context-keys -label:custom-editors -label:debug -label:debug-console -label:debug-disassembly -label:dialogs -label:diff-editor -label:dropdown -label:editor-api -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-RTL -label:editor-scrollbar -label:editor-sorting -label:editor-sticky-scroll -label:editor-sticky-scroll-decorations -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet-parse -label:extension-activation -label:extension-host -label:extension-prerelease -label:extension-recommendations -label:extension-signature -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-io -label:file-nesting -label:file-watcher -label:font-rendering -label:formatting -label:getting-started -label:ghost-text -label:git -label:github -label:github-repositories -label:gpu -label:grammar -label:grid-widget -label:icon-brand -label:icons-product -label:icons-widget -label:inlay-hints -label:inline-chat -label:inline-completions -label:install-update -label:intellisense-config -label:interactive-playground -label:interactive-window -label:javascript -label:json -label:json-sorting -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:L10N -label:l10n-platform -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list-widget -label:live-preview -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:merge-editor -label:merge-editor-workbench -label:monaco-editor -label:multi-monitor -label:native-file-dialog -label:network -label:notebook -label:notebook-accessibility -label:notebook-api -label:notebook-builtin-renderers -label:notebook-cell-editor -label:notebook-celltoolbar -label:notebook-clipboard -label:notebook-commands -label:notebook-commenting -label:notebook-debugging -label:notebook-diff -label:notebook-dnd -label:notebook-execution -label:notebook-find -label:notebook-folding -label:notebook-getting-started -label:notebook-globaltoolbar -label:notebook-ipynb -label:notebook-kernel -label:notebook-kernel-picker -label:notebook-language -label:notebook-layout -label:notebook-markdown -label:notebook-math -label:notebook-minimap -label:notebook-multiselect -label:notebook-output -label:notebook-perf -label:notebook-remote -label:notebook-rendering -label:notebook-serialization -label:notebook-serverless-web -label:notebook-statusbar -label:notebook-sticky-scroll -label:notebook-toc-outline -label:notebook-undo-redo -label:notebook-variables -label:notebook-workbench-integration -label:notebook-workflow -label:open-editors -label:opener -label:outline -label:output -label:packaging -label:panel-chat -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-open -label:quick-pick -label:quickpick-chat -label:references-viewlet -label:release-notes -label:remote -label:remote-connection -label:remote-desktop -label:remote-explorer -label:remote-tunnel -label:rename -label:runCommands -label:sandbox -label:sash-widget -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:server -label:settings-editor -label:settings-search -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview-widget -label:ssh -label:suggest -label:system-context-menu -label:table-widget -label:tasks -label:telemetry -label:terminal -label:terminal-accessibility -label:terminal-conpty -label:terminal-editors -label:terminal-external -label:terminal-find -label:terminal-input -label:terminal-layout -label:terminal-links -label:terminal-local-echo -label:terminal-persistence -label:terminal-process -label:terminal-profiles -label:terminal-quick-fix -label:terminal-rendering -label:terminal-shell-bash -label:terminal-shell-cmd -label:terminal-shell-fish -label:terminal-shell-git-bash -label:terminal-shell-integration -label:terminal-shell-pwsh -label:terminal-shell-zsh -label:terminal-tabs -label:terminal-winpty -label:testing -label:themes -label:timeline -label:timeline-git -label:timeline-local-history -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree-views -label:tree-widget -label:typescript -label:unc -label:undo-redo -label:unicode-highlight -label:uri -label:user-profiles -label:ux -label:variable-resolving -label:VIM -label:virtual-documents -label:virtual-workspaces -label:vscode-website -label:vscode.dev -label:web -label:webview -label:webview-views -label:workbench-actions -label:workbench-auxwindow -label:workbench-banner -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editor-groups -label:workbench-editor-resolver -label:workbench-editors -label:workbench-electron -label:workbench-feedback -label:workbench-fonts -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-untitled-editors -label:workbench-views -label:workbench-voice -label:workbench-welcome -label:workbench-window -label:workbench-workspace -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:workspace-trust -label:zoom -label:error-list -label:winget" + "value": "repo:microsoft/vscode assignee:@me is:open type:issue -label:\"info-needed\" -label:api -label:api-finalization -label:api-proposal -label:authentication -label:bisect-ext -label:bracket-pair-colorization -label:bracket-pair-guides -label:breadcrumbs -label:callhierarchy -label:chrome-devtools -label:code-lens -label:command-center -label:comments -label:config -label:context-keys -label:custom-editors -label:debug -label:debug-console -label:debug-disassembly -label:dialogs -label:diff-editor -label:dropdown -label:editor-api -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-RTL -label:editor-scrollbar -label:editor-sorting -label:editor-sticky-scroll -label:editor-sticky-scroll-decorations -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet-parse -label:extension-activation -label:extension-host -label:extension-prerelease -label:extension-recommendations -label:extension-signature -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-io -label:file-nesting -label:file-watcher -label:font-rendering -label:formatting -label:getting-started -label:ghost-text -label:git -label:github -label:github-repositories -label:gpu -label:grammar -label:grid-widget -label:icon-brand -label:icons-product -label:icons-widget -label:inlay-hints -label:inline-chat -label:inline-completions -label:install-update -label:intellisense-config -label:interactive-playground -label:interactive-window -label:javascript -label:json -label:json-sorting -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:L10N -label:l10n-platform -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list-widget -label:live-preview -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:merge-editor -label:merge-editor-workbench -label:monaco-editor -label:multi-monitor -label:native-file-dialog -label:network -label:notebook -label:notebook-accessibility -label:notebook-api -label:notebook-builtin-renderers -label:notebook-cell-editor -label:notebook-celltoolbar -label:notebook-clipboard -label:notebook-code-actions -label:notebook-commands -label:notebook-commenting -label:notebook-debugging -label:notebook-diff -label:notebook-dnd -label:notebook-execution -label:notebook-find -label:notebook-folding -label:notebook-format -label:notebook-getting-started -label:notebook-globaltoolbar -label:notebook-ipynb -label:notebook-kernel -label:notebook-kernel-picker -label:notebook-language -label:notebook-layout -label:notebook-markdown -label:notebook-minimap -label:notebook-multiselect -label:notebook-output -label:notebook-perf -label:notebook-remote -label:notebook-rendering -label:notebook-serialization -label:notebook-statusbar -label:notebook-sticky-scroll -label:notebook-toc-outline -label:notebook-undo-redo -label:notebook-variables -label:notebook-workbench-integration -label:notebook-workflow -label:open-editors -label:opener -label:outline -label:output -label:packaging -label:panel-chat -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-open -label:quick-pick -label:quickpick-chat -label:references-viewlet -label:release-notes -label:remote -label:remote-connection -label:remote-desktop -label:remote-explorer -label:remote-tunnel -label:rename -label:runCommands -label:sandbox -label:sash-widget -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:server -label:settings-editor -label:settings-search -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview-widget -label:ssh -label:suggest -label:system-context-menu -label:table-widget -label:tasks -label:telemetry -label:terminal -label:terminal-accessibility -label:terminal-conpty -label:terminal-editors -label:terminal-external -label:terminal-find -label:terminal-input -label:terminal-layout -label:terminal-links -label:terminal-local-echo -label:terminal-persistence -label:terminal-process -label:terminal-profiles -label:terminal-quick-fix -label:terminal-rendering -label:terminal-shell-bash -label:terminal-shell-cmd -label:terminal-shell-fish -label:terminal-shell-git-bash -label:terminal-shell-integration -label:terminal-shell-pwsh -label:terminal-shell-zsh -label:terminal-tabs -label:testing -label:themes -label:timeline -label:timeline-git -label:timeline-local-history -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree-views -label:tree-widget -label:typescript -label:unc -label:undo-redo -label:unicode-highlight -label:uri -label:user-profiles -label:ux -label:variable-resolving -label:VIM -label:virtual-documents -label:virtual-workspaces -label:vscode-website -label:vscode.dev -label:web -label:webview -label:webview-views -label:workbench-actions -label:workbench-auxwindow -label:workbench-banner -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editor-groups -label:workbench-editor-resolver -label:workbench-editors -label:workbench-electron -label:workbench-fonts -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-untitled-editors -label:workbench-views -label:workbench-voice -label:workbench-welcome -label:workbench-window -label:workbench-workspace -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:workspace-trust -label:zoom -label:error-list -label:winget" }, { "kind": 1, diff --git a/.vscode/settings.json b/.vscode/settings.json index 2ebf9705680..ee21d3328c3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -176,5 +176,6 @@ "editor.rulers": [ 100 ], - "typescript.enablePromptUseWorkspaceTsdk": true, + "inlineChat.experimental.textButtons": true, + "typescript.enablePromptUseWorkspaceTsdk": true } diff --git a/.yarnrc b/.yarnrc index b40fb7e7f58..b153fa4724f 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" target "29.4.0" -ms_build_id "9593362" +ms_build_id "9728852" runtime "electron" build_from_source "true" diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index a83316bd43e..73e5433b812 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -517,7 +517,7 @@ to the base-name name of the original file, and an extension of txt, html, or si --------------------------------------------------------- -go-syntax 0.6.6 - MIT +go-syntax 0.6.8 - MIT https://github.com/worlpaker/go-syntax MIT License @@ -833,7 +833,7 @@ SOFTWARE. --------------------------------------------------------- -jlelong/vscode-latex-basics 1.7.0 - MIT +jlelong/vscode-latex-basics 1.9.0 - MIT https://github.com/jlelong/vscode-latex-basics Copyright (c) vscode-latex-basics authors diff --git a/build/.webignore b/build/.webignore index 88fe96f5cc1..15935edce8a 100644 --- a/build/.webignore +++ b/build/.webignore @@ -20,6 +20,9 @@ vscode-textmate/webpack.config.js @xterm/xterm/src/** +@xterm/addon-clipboard/src/** +@xterm/addon-clipboard/out/** + @xterm/addon-image/src/** @xterm/addon-image/out/** diff --git a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml index dc8424f26ee..921bf2a9370 100644 --- a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml +++ b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml @@ -133,14 +133,6 @@ steps: VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:bionic-arm32v7 displayName: Install dependencies - - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: - - script: | - set -e - EXPECTED_GLIBC_VERSION="2.17" \ - EXPECTED_GLIBCXX_VERSION="3.4.19" \ - ./build/azure-pipelines/linux/verify-glibc-requirements.sh - displayName: Check GLIBC and GLIBCXX dependencies in remote/node_modules - - script: node build/azure-pipelines/distro/mixin-npm displayName: Mixin distro node modules @@ -172,9 +164,11 @@ steps: yarn gulp vscode-reh-linux-$(VSCODE_ARCH)-min-ci mv ../vscode-reh-linux-$(VSCODE_ARCH) ../vscode-server-linux-$(VSCODE_ARCH) # TODO@joaomoreno ARCHIVE_PATH=".build/linux/server/vscode-server-linux-legacy-$(VSCODE_ARCH).tar.gz" + UNARCHIVE_PATH="`pwd`/../vscode-server-linux-$(VSCODE_ARCH)" mkdir -p $(dirname $ARCHIVE_PATH) tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-linux-$(VSCODE_ARCH) echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" + echo "##vso[task.setvariable variable=SERVER_UNARCHIVE_PATH]$UNARCHIVE_PATH" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server @@ -192,6 +186,26 @@ steps: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server (web) + - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: + - script: | + set -e + EXPECTED_GLIBC_VERSION="2.17" \ + EXPECTED_GLIBCXX_VERSION="3.4.19" \ + ./build/azure-pipelines/linux/verify-glibc-requirements.sh + env: + SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) + displayName: Check GLIBC and GLIBCXX dependencies in server archive + + - ${{ else }}: + - script: | + set -e + EXPECTED_GLIBC_VERSION="2.28" \ + EXPECTED_GLIBCXX_VERSION="3.4.22" \ + ./build/azure-pipelines/linux/verify-glibc-requirements.sh + env: + SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) + displayName: Check GLIBC and GLIBCXX dependencies in server archive + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - template: product-build-linux-test.yml parameters: diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 352b31360f8..d1d6bdb9191 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -131,16 +131,6 @@ steps: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - script: | - set -e - - EXPECTED_GLIBC_VERSION="2.28" \ - EXPECTED_GLIBCXX_VERSION="3.4.25" \ - ./build/azure-pipelines/linux/verify-glibc-requirements.sh - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Check GLIBC and GLIBCXX dependencies in remote/node_modules - - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: node build/azure-pipelines/distro/mixin-npm condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) @@ -213,9 +203,11 @@ steps: yarn gulp vscode-reh-linux-$(VSCODE_ARCH)-min-ci mv ../vscode-reh-linux-$(VSCODE_ARCH) ../vscode-server-linux-$(VSCODE_ARCH) # TODO@joaomoreno ARCHIVE_PATH=".build/linux/server/vscode-server-linux-$(VSCODE_ARCH).tar.gz" + UNARCHIVE_PATH="`pwd`/../vscode-server-linux-$(VSCODE_ARCH)" mkdir -p $(dirname $ARCHIVE_PATH) tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-linux-$(VSCODE_ARCH) echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" + echo "##vso[task.setvariable variable=SERVER_UNARCHIVE_PATH]$UNARCHIVE_PATH" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server @@ -232,6 +224,36 @@ steps: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server (web) + - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: + - script: | + set -e + + source ./build/azure-pipelines/linux/setup-env.sh + + EXPECTED_GLIBC_VERSION="2.28" \ + EXPECTED_GLIBCXX_VERSION="3.4.25" \ + ./build/azure-pipelines/linux/verify-glibc-requirements.sh + env: + SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) + npm_config_arch: $(NPM_ARCH) + VSCODE_ARCH: $(VSCODE_ARCH) + displayName: Check GLIBC and GLIBCXX dependencies in server archive + + - ${{ else }}: + - script: | + set -e + + source ./build/azure-pipelines/linux/setup-env.sh + + EXPECTED_GLIBC_VERSION="2.28" \ + EXPECTED_GLIBCXX_VERSION="3.4.26" \ + ./build/azure-pipelines/linux/verify-glibc-requirements.sh + env: + SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) + npm_config_arch: $(NPM_ARCH) + VSCODE_ARCH: $(VSCODE_ARCH) + displayName: Check GLIBC and GLIBCXX dependencies in server archive + - ${{ else }}: - script: yarn gulp "transpile-client-swc" "transpile-extensions" env: diff --git a/build/azure-pipelines/linux/verify-glibc-requirements.sh b/build/azure-pipelines/linux/verify-glibc-requirements.sh index f07c0ba71b0..19482c242ea 100755 --- a/build/azure-pipelines/linux/verify-glibc-requirements.sh +++ b/build/azure-pipelines/linux/verify-glibc-requirements.sh @@ -9,8 +9,8 @@ elif [ "$VSCODE_ARCH" == "armhf" ]; then TRIPLE="arm-rpi-linux-gnueabihf" fi -# Get all files with .node extension from remote/node_modules folder -files=$(find remote/node_modules -name "*.node" -not -path "*prebuilds*") +# Get all files with .node extension from server folder +files=$(find $SEARCH_PATH -name "*.node" -not -path "*prebuilds*" -o -type f -executable -name "node") echo "Verifying requirements for files: $files" @@ -19,13 +19,13 @@ for file in $files; do glibcxx_version="$EXPECTED_GLIBCXX_VERSION" while IFS= read -r line; do if [[ $line == *"GLIBC_"* ]]; then - version=$(echo "$line" | awk '{print $5}' | tr -d '()') + version=$(echo "$line" | awk '{if ($5 ~ /^[0-9a-fA-F]+$/) print $6; else print $5}' | tr -d '()') version=${version#*_} if [[ $(printf "%s\n%s" "$version" "$glibc_version" | sort -V | tail -n1) == "$version" ]]; then glibc_version=$version fi elif [[ $line == *"GLIBCXX_"* ]]; then - version=$(echo "$line" | awk '{print $5}' | tr -d '()') + version=$(echo "$line" | awk '{if ($5 ~ /^[0-9a-fA-F]+$/) print $6; else print $5}' | tr -d '()') version=${version#*_} if [[ $(printf "%s\n%s" "$version" "$glibcxx_version" | sort -V | tail -n1) == "$version" ]]; then glibcxx_version=$version @@ -34,11 +34,11 @@ for file in $files; do done < <("$PWD/.build/sysroots/$TRIPLE/$TRIPLE/bin/objdump" -T "$file") if [[ "$glibc_version" != "$EXPECTED_GLIBC_VERSION" ]]; then - echo "Error: File $file has dependency on GLIBC > $EXPECTED_GLIBC_VERSION" + echo "Error: File $file has dependency on GLIBC > $EXPECTED_GLIBC_VERSION, found $glibc_version" exit 1 fi if [[ "$glibcxx_version" != "$EXPECTED_GLIBCXX_VERSION" ]]; then - echo "Error: File $file has dependency on GLIBCXX > $EXPECTED_GLIBCXX_VERSION" + echo "Error: File $file has dependency on GLIBCXX > $EXPECTED_GLIBCXX_VERSION, found $glibcxx_version" exit 1 fi done diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 0508059fcd7..5357697484e 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -271,7 +271,7 @@ extends: - job: CLIMacOSX64 pool: name: Azure Pipelines - image: macOS-11 + image: macOS-13 os: macOS steps: - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self @@ -284,7 +284,7 @@ extends: - job: CLIMacOSARM64 pool: name: Azure Pipelines - image: macOS-11 + image: macOS-13 os: macOS steps: - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self @@ -315,6 +315,18 @@ extends: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} + - stage: CustomSDL + dependsOn: [] + pool: + name: 1es-windows-2019-x64 + os: windows + jobs: + - job: WindowsSDL + variables: + - group: 'API Scan' + steps: + - template: build/azure-pipelines/sdl-scan.yml@self + - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_WINDOWS'], true)) }}: - stage: Windows dependsOn: @@ -597,7 +609,7 @@ extends: - CompileCLI pool: name: Azure Pipelines - image: macOS-11 + image: macOS-13 os: macOS variables: BUILDSECMON_OPT_IN: true diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 41c33f3f265..9a3748ed6fc 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -135,13 +135,22 @@ steps: - script: | set -e - AZURE_STORAGE_ACCOUNT="ticino" \ + AZURE_STORAGE_ACCOUNT="vscodeweb" \ AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ node build/azure-pipelines/upload-sourcemaps displayName: Upload sourcemaps to Azure + - script: | + set -e + AZURE_STORAGE_ACCOUNT="ticino" \ + AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ + AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ + AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ + node build/azure-pipelines/upload-sourcemaps + displayName: Upload sourcemaps to Azure (Deprecated) + - script: ./build/azure-pipelines/common/extract-telemetry.sh displayName: Generate lists of telemetry events diff --git a/build/azure-pipelines/sdl-scan.yml b/build/azure-pipelines/sdl-scan.yml index 927cd5e04ae..af20a305d9c 100644 --- a/build/azure-pipelines/sdl-scan.yml +++ b/build/azure-pipelines/sdl-scan.yml @@ -1,296 +1,151 @@ -trigger: none -pr: none - parameters: - name: NPM_REGISTRY displayName: "Custom NPM Registry" type: string default: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/npm/registry/" - - name: SCAN_WINDOWS - displayName: "Scan Windows" - type: boolean - default: true - - name: SCAN_LINUX - displayName: "Scan Linux" - type: boolean - default: false - -variables: - - name: NPM_REGISTRY - value: ${{ parameters.NPM_REGISTRY }} - - name: SCAN_WINDOWS - value: ${{ eq(parameters.SCAN_WINDOWS, true) }} - - name: SCAN_LINUX - value: ${{ eq(parameters.SCAN_LINUX, true) }} - - name: VSCODE_MIXIN_REPO - value: microsoft/vscode-distro - - name: skipComponentGovernanceDetection - value: true - name: NPM_ARCH - value: x64 + type: string + default: x64 - name: VSCODE_ARCH - value: x64 - - name: Codeql.enabled - value: true - - name: Codeql.TSAEnabled - value: true - - name: Codeql.TSAOptionsPath - value: '$(Build.SourcesDirectory)\build\azure-pipelines\config\tsaoptions.json' - -stages: - - stage: Windows - condition: eq(variables.SCAN_WINDOWS, 'true') - pool: 1es-windows-2019-x64 - jobs: - - job: WindowsJob - timeoutInMinutes: 0 - steps: - - task: CredScan@3 - continueOnError: true - inputs: - scanFolder: "$(Build.SourcesDirectory)" - outputFormat: "pre" - - - task: NodeTool@0 - inputs: - versionSource: fromFile - versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - - template: ./distro/download-distro.yml - - - task: AzureKeyVault@1 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: "vscode-builds-subscription" - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npm config set registry "$env:NPM_REGISTRY" --location=project } - # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb - # following is a workaround for yarn to send authorization header - # for GET requests to the registry. - exec { Add-Content -Path .npmrc -Value "always-auth=true" } - exec { yarn config set registry "$env:NPM_REGISTRY" } - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM & Yarn - - - task: npmAuthenticate@0 - inputs: - workingFile: .npmrc - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Authentication - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { node build/setup-npm-registry.js $env:NPM_REGISTRY } - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Registry - - - task: CodeQL3000Init@0 - displayName: CodeQL Initialize - condition: eq(variables['Codeql.enabled'], 'True') - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - . build/azure-pipelines/win32/retry.ps1 - $ErrorActionPreference = "Stop" - # TODO: remove custom node-gyp when updating to Node v20, - # refs https://github.com/npm/cli/releases/tag/v10.2.3 which is available with Node >= 20.10.0 - $nodeGypDir = "$(Agent.TempDirectory)/custom-packages" - mkdir "$nodeGypDir" - npm install node-gyp@10.0.1 -g --prefix "$nodeGypDir" - $env:npm_config_node_gyp = "${nodeGypDir}/node_modules/node-gyp/bin/node-gyp.js" - $env:npm_config_arch = "$(NPM_ARCH)" - retry { exec { yarn --frozen-lockfile --check-files } } - env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" - CHILD_CONCURRENCY: 1 - displayName: Install dependencies - - - script: node build/azure-pipelines/distro/mixin-npm - displayName: Mixin distro node modules - - - script: node build/azure-pipelines/distro/mixin-quality - displayName: Mixin distro quality - env: - VSCODE_QUALITY: stable - - - powershell: yarn compile - displayName: Compile - - - task: CodeQL3000Finalize@0 - displayName: CodeQL Finalize - condition: eq(variables['Codeql.enabled'], 'True') - - - powershell: yarn gulp "vscode-symbols-win32-$(VSCODE_ARCH)" - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Download Symbols - - - task: PSScriptAnalyzer@1 - inputs: - Path: '$(Build.SourcesDirectory)' - Settings: required - Recurse: true - - - task: BinSkim@4 - inputs: - InputType: "Basic" - Function: "analyze" - TargetPattern: "guardianGlob" - AnalyzeIgnorePdbLoadError: true - AnalyzeTargetGlob: '$(agent.builddirectory)\scanbin\**.dll;$(agent.builddirectory)\scanbin\**.exe;$(agent.builddirectory)\scanbin\**.node' - AnalyzeLocalSymbolDirectories: '$(agent.builddirectory)\scanbin\VSCode-win32-$(VSCODE_ARCH)\pdb' - - - task: AntiMalware@4 - inputs: - InputType: Basic - ScanType: CustomScan - FileDirPath: '$(Build.SourcesDirectory)' - EnableServices: true - SupportLogOnError: false - TreatSignatureUpdateFailureAs: 'Warning' - SignatureFreshness: 'OneDay' - TreatStaleSignatureAs: 'Error' - - - task: PublishSecurityAnalysisLogs@3 - inputs: - ArtifactName: CodeAnalysisLogs - ArtifactType: Container - PublishProcessedResults: false - AllTools: true - - - task: TSAUpload@2 - inputs: - GdnPublishTsaOnboard: true - GdnPublishTsaConfigFile: '$(Build.SourcesDirectory)\build\azure-pipelines\config\tsaoptions.json' - - - stage: Linux - dependsOn: [] - condition: eq(variables.SCAN_LINUX, 'true') - pool: - vmImage: "Ubuntu-18.04" - jobs: - - job: LinuxJob - steps: - - task: CredScan@2 - inputs: - toolMajorVersion: "V2" - - task: NodeTool@0 - inputs: - versionSource: fromFile - versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - - template: ./distro/download-distro.yml - - - task: AzureKeyVault@1 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: "vscode-builds-subscription" - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" - - - script: | - set -e - npm config set registry "$NPM_REGISTRY" --location=project - # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb - # following is a workaround for yarn to send authorization header - # for GET requests to the registry. - echo "always-auth=true" >> .npmrc - yarn config set registry "$NPM_REGISTRY" - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM & Yarn - - - task: npmAuthenticate@0 - inputs: - workingFile: .npmrc - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Authentication - - - script: node build/setup-npm-registry.js $NPM_REGISTRY - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Registry - - - script: | - set -e - for i in {1..5}; do # try 5 times - yarn --cwd build --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." - done - displayName: Install build dependencies - - - script: | - set -e - export npm_config_arch=$(NPM_ARCH) - - if [ -z "$CC" ] || [ -z "$CXX" ]; then - # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/96.0.4664.110/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux - # Download libcxx headers and objects from upstream electron releases - DEBUG=libcxx-fetcher \ - VSCODE_LIBCXX_OBJECTS_DIR=$PWD/.build/libcxx-objects \ - VSCODE_LIBCXX_HEADERS_DIR=$PWD/.build/libcxx_headers \ - VSCODE_LIBCXXABI_HEADERS_DIR=$PWD/.build/libcxxabi_headers \ - VSCODE_ARCH="$(NPM_ARCH)" \ - node build/linux/libcxx-fetcher.js - # Set compiler toolchain - export CC=$PWD/.build/CR_Clang/bin/clang - export CXX=$PWD/.build/CR_Clang/bin/clang++ - export CXXFLAGS="-std=c++17 -nostdinc++ -D__NO_INLINE__ -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr" - export LDFLAGS="-stdlib=libc++ -fuse-ld=lld -flto=thin -fsplit-lto-unit -L$PWD/.build/libcxx-objects -lc++abi" - export VSCODE_REMOTE_CC=$(which gcc) - export VSCODE_REMOTE_CXX=$(which g++) - fi - - for i in {1..5}; do # try 5 times - yarn --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." - done - env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Install dependencies - - - script: yarn --frozen-lockfile --check-files - workingDirectory: .build/distro/npm - env: - npm_config_arch: $(NPM_ARCH) - displayName: Install distro node modules - - - script: node build/azure-pipelines/distro/mixin-npm - displayName: Mixin distro node modules - - - script: node build/azure-pipelines/distro/mixin-quality - displayName: Mixin distro quality - env: - VSCODE_QUALITY: stable - - - script: yarn gulp vscode-symbols-linux-$(VSCODE_ARCH) - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Build - - - task: BinSkim@3 - inputs: - toolVersion: Latest - InputType: CommandLine - arguments: analyze $(agent.builddirectory)\scanbin\exe\*.* --recurse --local-symbol-directories $(agent.builddirectory)\scanbin\VSCode-linux-$(VSCODE_ARCH)\pdb - - - task: TSAUpload@2 - inputs: - GdnPublishTsaConfigFile: '$(Build.SourceDirectory)\build\azure-pipelines\config\tsaoptions.json' + type: string + default: x64 + +steps: + - task: NodeTool@0 + inputs: + versionSource: fromFile + versionFilePath: .nvmrc + nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + + - template: ./distro/download-distro.yml + + - task: AzureKeyVault@1 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: "vscode-builds-subscription" + KeyVaultName: vscode-build-secrets + SecretsFilter: "github-distro-mixin-password" + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm config set registry "${{ parameters.NPM_REGISTRY }}" --location=project } + # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb + # following is a workaround for yarn to send authorization header + # for GET requests to the registry. + exec { Add-Content -Path .npmrc -Value "always-auth=true" } + exec { yarn config set registry "${{ parameters.NPM_REGISTRY }}" } + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne('${{ parameters.NPM_REGISTRY }}', 'none')) + displayName: Setup NPM & Yarn + + - task: npmAuthenticate@0 + inputs: + workingFile: .npmrc + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne('${{ parameters.NPM_REGISTRY }}', 'none')) + displayName: Setup NPM Authentication + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { node build/setup-npm-registry.js "${{ parameters.NPM_REGISTRY }}" } + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne('${{ parameters.NPM_REGISTRY }}', 'none')) + displayName: Setup NPM Registry + + - pwsh: | + $includes = @' + { + 'target_defaults': { + 'conditions': [ + ['OS=="win"', { + 'msvs_configuration_attributes': { + 'SpectreMitigation': 'Spectre' + }, + 'msvs_settings': { + 'VCCLCompilerTool': { + 'AdditionalOptions': [ + '/Zi', + '/FS' + ], + }, + 'VCLinkerTool': { + 'AdditionalOptions': [ + '/profile' + ] + } + } + }] + ] + } + } + '@ + + if (!(Test-Path "~/.gyp")) { + mkdir "~/.gyp" + } + echo $includes > "~/.gyp/include.gypi" + displayName: Create include.gypi + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + . build/azure-pipelines/win32/retry.ps1 + $ErrorActionPreference = "Stop" + retry { exec { yarn --frozen-lockfile --check-files } } + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: "$(github-distro-mixin-password)" + CHILD_CONCURRENCY: 1 + displayName: Install dependencies + + - script: node build/azure-pipelines/distro/mixin-npm + displayName: Mixin distro node modules + + - script: node build/azure-pipelines/distro/mixin-quality + displayName: Mixin distro quality + env: + VSCODE_QUALITY: stable + + - powershell: yarn compile + displayName: Compile + + - powershell: yarn gulp "vscode-symbols-win32-${{ parameters.VSCODE_ARCH }}" + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Download Symbols + + - task: BinSkim@4 + inputs: + InputType: "Basic" + Function: "analyze" + TargetPattern: "guardianGlob" + AnalyzeIgnorePdbLoadError: true + AnalyzeTargetGlob: '$(agent.builddirectory)\scanbin\**.dll;$(agent.builddirectory)\scanbin\**.exe;$(agent.builddirectory)\scanbin\**.node' + AnalyzeLocalSymbolDirectories: '$(agent.builddirectory)\scanbin\VSCode-win32-${{ parameters.VSCODE_ARCH }}\pdb' + + - task: CopyFiles@2 + displayName: 'Collect Symbols for API Scan' + inputs: + SourceFolder: $(Agent.BuildDirectory) + Contents: 'scanbin\**\*.pdb' + TargetFolder: '$(agent.builddirectory)\symbols' + flattenFolders: true + condition: succeeded() + + # - task: APIScan@2 + # inputs: + # softwareFolder: $(agent.builddirectory)\scanbin + # softwareName: 'vscode-client' + # softwareVersionNum: '1' + # symbolsFolder: 'SRV*http://symweb;$(agent.builddirectory)\symbols' + # isLargeApp: false + # toolVersion: 'Latest' + # displayName: Run ApiScan + # condition: succeeded() + # env: + # AzureServicesAuthConnectionString: $(apiscan-connectionstring) + + - task: PublishSecurityAnalysisLogs@3 + inputs: + ArtifactName: CodeAnalysisLogs + ArtifactType: Container + PublishProcessedResults: false + AllTools: true diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index bf43d9212cf..5e423d077c7 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -129,6 +129,15 @@ steps: node build/azure-pipelines/upload-cdn displayName: Upload to CDN + - script: | + set -e + AZURE_STORAGE_ACCOUNT="vscodeweb" \ + AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ + AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ + AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ + node build/azure-pipelines/upload-sourcemaps out-vscode-web-min out-vscode-web-min/vs/workbench/workbench.web.main.js.map + displayName: Upload sourcemaps (Web) + # upload only the workbench.web.main.js source maps because # we just compiled these bits in the previous step and the # general task to upload source maps has already been run @@ -139,7 +148,7 @@ steps: AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ node build/azure-pipelines/upload-sourcemaps out-vscode-web-min out-vscode-web-min/vs/workbench/workbench.web.main.js.map - displayName: Upload sourcemaps (Web) + displayName: Upload sourcemaps (Deprecated) - script: | set -e diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/product-build-win32-test.yml index a3b251b71ac..ce791c094e6 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/product-build-win32-test.yml @@ -72,6 +72,11 @@ steps: } displayName: Build integration tests + - powershell: .\build\azure-pipelines\win32\listprocesses.bat + displayName: Diagnostics before integration test runs + continueOnError: true + condition: succeededOrFailed() + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: - powershell: .\scripts\test-integration.bat --tfs "Integration Tests" displayName: Run integration tests (Electron) @@ -121,6 +126,11 @@ steps: displayName: Run integration tests (Remote) timeoutInMinutes: 20 + - powershell: .\build\azure-pipelines\win32\listprocesses.bat + displayName: Diagnostics after integration test runs + continueOnError: true + condition: succeededOrFailed() + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - powershell: .\build\azure-pipelines\win32\listprocesses.bat displayName: Diagnostics before smoke test run diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index aec900d488e..c714bca3280 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -85,7 +85,7 @@ const compilations = [ '.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json', ]; -const getBaseUrl = out => `https://ticino.blob.core.windows.net/sourcemaps/${commit}/${out}`; +const getBaseUrl = out => `https://main.vscode-cdn.net/sourcemaps/${commit}/${out}`; const tasks = compilations.map(function (tsconfigFile) { const absolutePath = path.join(root, tsconfigFile); diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 9623bf6dbb8..9513e928bc2 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -132,7 +132,8 @@ function getNodeChecksum(nodeVersion, platform, arch, glibcPrefix) { let expectedName; switch (platform) { case 'win32': - expectedName = `win-${arch}/node.exe`; + expectedName = product.nodejsRepository !== 'https://nodejs.org' ? + `win-${arch}-node.exe` : `win-${arch}/node.exe`; break; case 'darwin': @@ -463,7 +464,7 @@ function tweakProductForServerWeb(product) { const minifyTask = task.define(`minify-vscode-${type}`, task.series( optimizeTask, util.rimraf(`out-vscode-${type}-min`), - optimize.minifyTask(`out-vscode-${type}`, `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) + optimize.minifyTask(`out-vscode-${type}`, `https://main.vscode-cdn.net/sourcemaps/${commit}/core`) )); gulp.task(minifyTask); diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 427dcefaedd..263bc6a21e2 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -142,7 +142,7 @@ const optimizeVSCodeTask = task.define('optimize-vscode', task.series( )); gulp.task(optimizeVSCodeTask); -const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${commit}`; +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; const minifyVSCodeTask = task.define('minify-vscode', task.series( optimizeVSCodeTask, util.rimraf('out-vscode-min'), diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 7c412457e47..9c643fa7e58 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -8,10 +8,9 @@ const gulp = require('gulp'); const replace = require('gulp-replace'); const rename = require('gulp-rename'); -const shell = require('gulp-shell'); const es = require('event-stream'); const vfs = require('vinyl-fs'); -const util = require('./lib/util'); +const { rimraf } = require('./lib/util'); const { getVersion } = require('./lib/getVersion'); const task = require('./lib/task'); const packageJson = require('../package.json'); @@ -19,6 +18,10 @@ const product = require('../product.json'); const dependenciesGenerator = require('./linux/dependencies-generator'); const debianRecommendedDependencies = require('./linux/debian/dep-lists').recommendedDeps; const path = require('path'); +const cp = require('child_process'); +const util = require('util'); + +const exec = util.promisify(cp.exec); const root = path.dirname(__dirname); const commit = getVersion(root); @@ -129,11 +132,13 @@ function prepareDebPackage(arch) { */ function buildDebPackage(arch) { const debArch = getDebPackageArch(arch); - return shell.task([ - 'chmod 755 ' + product.applicationName + '-' + debArch + '/DEBIAN/postinst ' + product.applicationName + '-' + debArch + '/DEBIAN/prerm ' + product.applicationName + '-' + debArch + '/DEBIAN/postrm', - 'mkdir -p deb', - 'fakeroot dpkg-deb -b ' + product.applicationName + '-' + debArch + ' deb' - ], { cwd: '.build/linux/deb/' + debArch }); + const cwd = `.build/linux/deb/${debArch}`; + + return async () => { + await exec(`chmod 755 ${product.applicationName}-${debArch}/DEBIAN/postinst ${product.applicationName}-${debArch}/DEBIAN/prerm ${product.applicationName}-${debArch}/DEBIAN/postrm`, { cwd }); + await exec('mkdir -p deb', { cwd }); + await exec(`fakeroot dpkg-deb -b ${product.applicationName}-${debArch} deb`, { cwd }); + }; } /** @@ -241,14 +246,14 @@ function prepareRpmPackage(arch) { function buildRpmPackage(arch) { const rpmArch = getRpmPackageArch(arch); const rpmBuildPath = getRpmBuildPath(rpmArch); - const rpmOut = rpmBuildPath + '/RPMS/' + rpmArch; - const destination = '.build/linux/rpm/' + rpmArch; - - return shell.task([ - 'mkdir -p ' + destination, - 'HOME="$(pwd)/' + destination + '" rpmbuild -bb ' + rpmBuildPath + '/SPECS/' + product.applicationName + '.spec --target=' + rpmArch, - 'cp "' + rpmOut + '/$(ls ' + rpmOut + ')" ' + destination + '/' - ]); + const rpmOut = `${rpmBuildPath}/RPMS/${rpmArch}`; + const destination = `.build/linux/rpm/${rpmArch}`; + + return async () => { + await exec(`mkdir -p ${destination}`); + await exec(`HOME="$(pwd)/${destination}" rpmbuild -bb ${rpmBuildPath}/SPECS/${product.applicationName}.spec --target=${rpmArch}`); + await exec(`cp "${rpmOut}/$(ls ${rpmOut})" ${destination}/`); + }; } /** @@ -311,9 +316,8 @@ function prepareSnapPackage(arch) { * @param {string} arch */ function buildSnapPackage(arch) { - const snapBuildPath = getSnapBuildPath(arch); - // Default target for snapcraft runs: pull, build, stage and prime, and finally assembles the snap. - return shell.task(`cd ${snapBuildPath} && snapcraft`); + const cwd = getSnapBuildPath(arch); + return () => exec('snapcraft', { cwd }); } const BUILD_TARGETS = [ @@ -324,18 +328,18 @@ const BUILD_TARGETS = [ BUILD_TARGETS.forEach(({ arch }) => { const debArch = getDebPackageArch(arch); - const prepareDebTask = task.define(`vscode-linux-${arch}-prepare-deb`, task.series(util.rimraf(`.build/linux/deb/${debArch}`), prepareDebPackage(arch))); + const prepareDebTask = task.define(`vscode-linux-${arch}-prepare-deb`, task.series(rimraf(`.build/linux/deb/${debArch}`), prepareDebPackage(arch))); gulp.task(prepareDebTask); const buildDebTask = task.define(`vscode-linux-${arch}-build-deb`, buildDebPackage(arch)); gulp.task(buildDebTask); const rpmArch = getRpmPackageArch(arch); - const prepareRpmTask = task.define(`vscode-linux-${arch}-prepare-rpm`, task.series(util.rimraf(`.build/linux/rpm/${rpmArch}`), prepareRpmPackage(arch))); + const prepareRpmTask = task.define(`vscode-linux-${arch}-prepare-rpm`, task.series(rimraf(`.build/linux/rpm/${rpmArch}`), prepareRpmPackage(arch))); gulp.task(prepareRpmTask); const buildRpmTask = task.define(`vscode-linux-${arch}-build-rpm`, buildRpmPackage(arch)); gulp.task(buildRpmTask); - const prepareSnapTask = task.define(`vscode-linux-${arch}-prepare-snap`, task.series(util.rimraf(`.build/linux/snap/${arch}`), prepareSnapPackage(arch))); + const prepareSnapTask = task.define(`vscode-linux-${arch}-prepare-snap`, task.series(rimraf(`.build/linux/snap/${arch}`), prepareSnapPackage(arch))); gulp.task(prepareSnapTask); const buildSnapTask = task.define(`vscode-linux-${arch}-build-snap`, task.series(prepareSnapTask, buildSnapPackage(arch))); gulp.task(buildSnapTask); diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index 9edcff5a931..0ca23fd8f75 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -191,7 +191,7 @@ const optimizeVSCodeWebTask = task.define('optimize-vscode-web', task.series( const minifyVSCodeWebTask = task.define('minify-vscode-web', task.series( optimizeVSCodeWebTask, util.rimraf('out-vscode-web-min'), - optimize.minifyTask('out-vscode-web', `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) + optimize.minifyTask('out-vscode-web', `https://main.vscode-cdn.net/sourcemaps/${commit}/core`) )); gulp.task(minifyVSCodeWebTask); diff --git a/build/lib/bundle.js b/build/lib/bundle.js index 61d9f015624..1a2f870991f 100644 --- a/build/lib/bundle.js +++ b/build/lib/bundle.js @@ -231,6 +231,9 @@ function removeDuplicateTSBoilerplate(destFiles) { { start: /^var __param/, end: /^};$/ }, { start: /^var __awaiter/, end: /^};$/ }, { start: /^var __generator/, end: /^};$/ }, + { start: /^var __createBinding/, end: /^}\)\);$/ }, + { start: /^var __setModuleDefault/, end: /^}\);$/ }, + { start: /^var __importStar/, end: /^};$/ }, ]; destFiles.forEach((destFile) => { const SEEN_BOILERPLATE = []; diff --git a/build/lib/bundle.ts b/build/lib/bundle.ts index c5fdc2da18c..2efa081b471 100644 --- a/build/lib/bundle.ts +++ b/build/lib/bundle.ts @@ -365,6 +365,9 @@ function removeDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[] { { start: /^var __param/, end: /^};$/ }, { start: /^var __awaiter/, end: /^};$/ }, { start: /^var __generator/, end: /^};$/ }, + { start: /^var __createBinding/, end: /^}\)\);$/ }, + { start: /^var __setModuleDefault/, end: /^}\);$/ }, + { start: /^var __importStar/, end: /^};$/ }, ]; destFiles.forEach((destFile) => { diff --git a/build/lib/compilation.js b/build/lib/compilation.js index 5db8e5ed3e8..13f4aa0ee9b 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -237,7 +237,7 @@ class MonacoGenerator { function generateApiProposalNames() { let eol; try { - const src = fs.readFileSync('src/vs/workbench/services/extensions/common/extensionsApiProposals.ts', 'utf-8'); + const src = fs.readFileSync('src/vs/platform/extensions/common/extensionsApiProposals.ts', 'utf-8'); const match = /\r?\n/m.exec(src); eol = match ? match[0] : os.EOL; } @@ -245,18 +245,27 @@ function generateApiProposalNames() { eol = os.EOL; } const pattern = /vscode\.proposed\.([a-zA-Z\d]+)\.d\.ts$/; - const proposalNames = new Set(); + const versionPattern = /^\s*\/\/\s*version\s*:\s*(\d+)\s*$/mi; + const proposals = new Map(); const input = es.through(); const output = input .pipe(util.filter((f) => pattern.test(f.path))) .pipe(es.through((f) => { const name = path.basename(f.path); const match = pattern.exec(name); - if (match) { - proposalNames.add(match[1]); + if (!match) { + return; } + const proposalName = match[1]; + const contents = f.contents.toString('utf8'); + const versionMatch = versionPattern.exec(contents); + const version = versionMatch ? versionMatch[1] : undefined; + proposals.set(proposalName, { + proposal: `https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.${proposalName}.d.ts`, + version: version ? parseInt(version) : undefined + }); }, function () { - const names = [...proposalNames.values()].sort(); + const names = [...proposals.keys()].sort(); const contents = [ '/*---------------------------------------------------------------------------------------------', ' * Copyright (c) Microsoft Corporation. All rights reserved.', @@ -265,14 +274,18 @@ function generateApiProposalNames() { '', '// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY.', '', - 'export const allApiProposals = Object.freeze({', - `${names.map(name => `\t${name}: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.${name}.d.ts'`).join(`,${eol}`)}`, - '});', - 'export type ApiProposalName = keyof typeof allApiProposals;', + 'const _allApiProposals = {', + `${names.map(proposalName => { + const proposal = proposals.get(proposalName); + return `\t${proposalName}: {${eol}\t\tproposal: '${proposal.proposal}',${eol}${proposal.version ? `\t\tversion: ${proposal.version}${eol}` : ''}\t}`; + }).join(`,${eol}`)}`, + '};', + 'export const allApiProposals = Object.freeze<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>(_allApiProposals);', + 'export type ApiProposalName = keyof typeof _allApiProposals;', '', ].join(eol); this.emit('data', new File({ - path: 'vs/workbench/services/extensions/common/extensionsApiProposals.ts', + path: 'vs/platform/extensions/common/extensionsApiProposals.ts', contents: Buffer.from(contents) })); this.emit('end'); diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index cdccb13f0cd..d8876a58e50 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -278,7 +278,7 @@ function generateApiProposalNames() { let eol: string; try { - const src = fs.readFileSync('src/vs/workbench/services/extensions/common/extensionsApiProposals.ts', 'utf-8'); + const src = fs.readFileSync('src/vs/platform/extensions/common/extensionsApiProposals.ts', 'utf-8'); const match = /\r?\n/m.exec(src); eol = match ? match[0] : os.EOL; } catch { @@ -286,7 +286,8 @@ function generateApiProposalNames() { } const pattern = /vscode\.proposed\.([a-zA-Z\d]+)\.d\.ts$/; - const proposalNames = new Set(); + const versionPattern = /^\s*\/\/\s*version\s*:\s*(\d+)\s*$/mi; + const proposals = new Map(); const input = es.through(); const output = input @@ -295,11 +296,22 @@ function generateApiProposalNames() { const name = path.basename(f.path); const match = pattern.exec(name); - if (match) { - proposalNames.add(match[1]); + if (!match) { + return; } + + const proposalName = match[1]; + + const contents = f.contents.toString('utf8'); + const versionMatch = versionPattern.exec(contents); + const version = versionMatch ? versionMatch[1] : undefined; + + proposals.set(proposalName, { + proposal: `https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.${proposalName}.d.ts`, + version: version ? parseInt(version) : undefined + }); }, function () { - const names = [...proposalNames.values()].sort(); + const names = [...proposals.keys()].sort(); const contents = [ '/*---------------------------------------------------------------------------------------------', ' * Copyright (c) Microsoft Corporation. All rights reserved.', @@ -308,15 +320,19 @@ function generateApiProposalNames() { '', '// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY.', '', - 'export const allApiProposals = Object.freeze({', - `${names.map(name => `\t${name}: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.${name}.d.ts'`).join(`,${eol}`)}`, - '});', - 'export type ApiProposalName = keyof typeof allApiProposals;', + 'const _allApiProposals = {', + `${names.map(proposalName => { + const proposal = proposals.get(proposalName)!; + return `\t${proposalName}: {${eol}\t\tproposal: '${proposal.proposal}',${eol}${proposal.version ? `\t\tversion: ${proposal.version}${eol}` : ''}\t}`; + }).join(`,${eol}`)}`, + '};', + 'export const allApiProposals = Object.freeze<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>(_allApiProposals);', + 'export type ApiProposalName = keyof typeof _allApiProposals;', '', ].join(eol); this.emit('data', new File({ - path: 'vs/workbench/services/extensions/common/extensionsApiProposals.ts', + path: 'vs/platform/extensions/common/extensionsApiProposals.ts', contents: Buffer.from(contents) })); this.emit('end'); diff --git a/build/lib/electron.js b/build/lib/electron.js index e3f42fe6cc1..91f695cbd74 100644 --- a/build/lib/electron.js +++ b/build/lib/electron.js @@ -54,7 +54,7 @@ function darwinBundleDocumentType(extensions, icon, nameOrSuffix, utis) { role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], extensions, - iconFile: 'resources/darwin/' + icon + '.icns', + iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', utis }; } diff --git a/build/lib/electron.ts b/build/lib/electron.ts index 127a7a86119..b53ab5b53ac 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -68,7 +68,7 @@ function darwinBundleDocumentType(extensions: string[], icon: string, nameOrSuff role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], extensions, - iconFile: 'resources/darwin/' + icon + '.icns', + iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', utis }; } diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 4892f7af40a..6d2533b2f4d 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -38,7 +38,7 @@ const util_1 = require("./util"); // --- End Positron --- const root = path.dirname(path.dirname(__dirname)); const commit = (0, getVersion_1.getVersion)(root); -const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${commit}`; +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input) { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); return input diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index b3a30253631..c7292311f90 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -32,7 +32,7 @@ import { PromiseHandles } from './util'; const root = path.dirname(path.dirname(__dirname)); const commit = getVersion(root); -const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${commit}`; +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input: Stream): Stream { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index dce2b85d658..7494b71bb66 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -68,7 +68,8 @@ const CORE_TYPES = [ 'fetch', 'RequestInit', 'Headers', - 'Response' + 'Response', + '__global' ]; // Types that are defined in a common layer but are known to be only // available in native environments should not be allowed in browser @@ -170,59 +171,17 @@ const RULES = [ '@types/node' // no node.js ] }, - // Common: vs/workbench/api/common/extHostTypes.ts + // Common: vs/base/parts/sandbox/electron-sandbox/preload.js { - target: '**/vs/workbench/api/common/extHostTypes.ts', + target: '**/vs/base/parts/sandbox/electron-sandbox/preload.js', allowedTypes: [ ...CORE_TYPES, - // Safe access to global - '__global' - ], - disallowedTypes: NATIVE_TYPES, - disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM - '@types/node' // no node.js - ] - }, - // Common: vs/workbench/api/common/extHostChatAgents2.ts - { - target: '**/vs/workbench/api/common/extHostChatAgents2.ts', - allowedTypes: [ - ...CORE_TYPES, - // Safe access to global - '__global' + // Safe access to a very small subset of node.js + 'process', + 'NodeJS' ], disallowedTypes: NATIVE_TYPES, disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM - '@types/node' // no node.js - ] - }, - // Common: vs/workbench/api/common/extHostChatVariables.ts - { - target: '**/vs/workbench/api/common/extHostChatVariables.ts', - allowedTypes: [ - ...CORE_TYPES, - // Safe access to global - '__global' - ], - disallowedTypes: NATIVE_TYPES, - disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM - '@types/node' // no node.js - ] - }, - // Common: vs/workbench/api/common/extensionHostMain.ts - { - target: '**/vs/workbench/api/common/extensionHostMain.ts', - allowedTypes: [ - ...CORE_TYPES, - // Safe access to global - '__global' - ], - disallowedTypes: NATIVE_TYPES, - disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM '@types/node' // no node.js ] }, diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 039f222135d..4861fa6d86e 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -69,7 +69,8 @@ const CORE_TYPES = [ 'fetch', 'RequestInit', 'Headers', - 'Response' + 'Response', + '__global' ]; // Types that are defined in a common layer but are known to be only @@ -185,66 +186,18 @@ const RULES: IRule[] = [ ] }, - // Common: vs/workbench/api/common/extHostTypes.ts + // Common: vs/base/parts/sandbox/electron-sandbox/preload.js { - target: '**/vs/workbench/api/common/extHostTypes.ts', + target: '**/vs/base/parts/sandbox/electron-sandbox/preload.js', allowedTypes: [ ...CORE_TYPES, - // Safe access to global - '__global' + // Safe access to a very small subset of node.js + 'process', + 'NodeJS' ], disallowedTypes: NATIVE_TYPES, disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM - '@types/node' // no node.js - ] - }, - - // Common: vs/workbench/api/common/extHostChatAgents2.ts - { - target: '**/vs/workbench/api/common/extHostChatAgents2.ts', - allowedTypes: [ - ...CORE_TYPES, - - // Safe access to global - '__global' - ], - disallowedTypes: NATIVE_TYPES, - disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM - '@types/node' // no node.js - ] - }, - - // Common: vs/workbench/api/common/extHostChatVariables.ts - { - target: '**/vs/workbench/api/common/extHostChatVariables.ts', - allowedTypes: [ - ...CORE_TYPES, - - // Safe access to global - '__global' - ], - disallowedTypes: NATIVE_TYPES, - disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM - '@types/node' // no node.js - ] - }, - - // Common: vs/workbench/api/common/extensionHostMain.ts - { - target: '**/vs/workbench/api/common/extensionHostMain.ts', - allowedTypes: [ - ...CORE_TYPES, - - // Safe access to global - '__global' - ], - disallowedTypes: NATIVE_TYPES, - disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM '@types/node' // no node.js ] }, diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 70205ae11dd..9b67ee52af3 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -94,6 +94,7 @@ "--vscode-debugTokenExpression-name", "--vscode-debugTokenExpression-number", "--vscode-debugTokenExpression-string", + "--vscode-debugTokenExpression-type", "--vscode-debugTokenExpression-value", "--vscode-debugToolBar-background", "--vscode-debugToolBar-border", @@ -128,15 +129,16 @@ "--vscode-dropdown-listBackground", "--vscode-editor-background", "--vscode-editor-findMatchBackground", - "--vscode-editor-findMatchForeground", "--vscode-editor-findMatchBorder", + "--vscode-editor-findMatchForeground", "--vscode-editor-findMatchHighlightBackground", - "--vscode-editor-findMatchHighlightForeground", "--vscode-editor-findMatchHighlightBorder", + "--vscode-editor-findMatchHighlightForeground", "--vscode-editor-findRangeHighlightBackground", "--vscode-editor-findRangeHighlightBorder", "--vscode-editor-focusedStackFrameHighlightBackground", "--vscode-editor-foldBackground", + "--vscode-editor-foldPlaceholderForeground", "--vscode-editor-foreground", "--vscode-editor-hoverHighlightBackground", "--vscode-editor-inactiveSelectionBackground", @@ -145,6 +147,7 @@ "--vscode-editor-lineHighlightBackground", "--vscode-editor-lineHighlightBorder", "--vscode-editor-linkedEditingBackground", + "--vscode-editor-placeholder-foreground", "--vscode-editor-rangeHighlightBackground", "--vscode-editor-rangeHighlightBorder", "--vscode-editor-selectionBackground", @@ -491,12 +494,12 @@ "--vscode-panelSectionHeader-background", "--vscode-panelSectionHeader-border", "--vscode-panelSectionHeader-foreground", - "--vscode-panelTitle-activeBorder", - "--vscode-panelTitle-activeForeground", - "--vscode-panelTitle-inactiveForeground", "--vscode-panelStickyScroll-background", "--vscode-panelStickyScroll-border", "--vscode-panelStickyScroll-shadow", + "--vscode-panelTitle-activeBorder", + "--vscode-panelTitle-activeForeground", + "--vscode-panelTitle-inactiveForeground", "--vscode-peekView-border", "--vscode-peekViewEditor-background", "--vscode-peekViewEditor-matchHighlightBackground", @@ -683,6 +686,7 @@ "--vscode-problemsWarningIcon-foreground", "--vscode-profileBadge-background", "--vscode-profileBadge-foreground", + "--vscode-profiles-sashBorder", "--vscode-progressBar-background", "--vscode-quickInput-background", "--vscode-quickInput-foreground", @@ -734,11 +738,11 @@ "--vscode-sideBarSectionHeader-background", "--vscode-sideBarSectionHeader-border", "--vscode-sideBarSectionHeader-foreground", - "--vscode-sideBarTitle-background", - "--vscode-sideBarTitle-foreground", "--vscode-sideBarStickyScroll-background", "--vscode-sideBarStickyScroll-border", "--vscode-sideBarStickyScroll-shadow", + "--vscode-sideBarTitle-background", + "--vscode-sideBarTitle-foreground", "--vscode-sideBySideEditor-horizontalBorder", "--vscode-sideBySideEditor-verticalBorder", "--vscode-simpleFindWidget-sashBorder", @@ -813,9 +817,6 @@ "--vscode-tab-activeBackground", "--vscode-tab-activeBorder", "--vscode-tab-activeBorderTop", - "--vscode-tab-selectedBorderTop", - "--vscode-tab-selectedBackground", - "--vscode-tab-selectedForeground", "--vscode-tab-activeForeground", "--vscode-tab-activeModifiedBorder", "--vscode-tab-border", @@ -827,6 +828,9 @@ "--vscode-tab-inactiveForeground", "--vscode-tab-inactiveModifiedBorder", "--vscode-tab-lastPinnedBorder", + "--vscode-tab-selectedBackground", + "--vscode-tab-selectedBorderTop", + "--vscode-tab-selectedForeground", "--vscode-tab-unfocusedActiveBackground", "--vscode-tab-unfocusedActiveBorder", "--vscode-tab-unfocusedActiveBorderTop", @@ -864,6 +868,7 @@ "--vscode-terminal-foreground", "--vscode-terminal-hoverHighlightBackground", "--vscode-terminal-inactiveSelectionBackground", + "--vscode-terminal-initialHintForeground", "--vscode-terminal-selectionBackground", "--vscode-terminal-selectionForeground", "--vscode-terminal-tab-activeBorder", @@ -875,6 +880,7 @@ "--vscode-terminalOverviewRuler-cursorForeground", "--vscode-terminalOverviewRuler-findMatchForeground", "--vscode-terminalStickyScroll-background", + "--vscode-terminalStickyScroll-border", "--vscode-terminalStickyScrollHover-background", "--vscode-testing-coverCountBadgeBackground", "--vscode-testing-coverCountBadgeForeground", @@ -979,8 +985,6 @@ "--vscode-hover-maxWidth", "--vscode-hover-sourceWhiteSpace", "--vscode-hover-whiteSpace", - "--vscode-inline-chat-quick-voice-height", - "--vscode-inline-chat-quick-voice-width", "--vscode-editor-dictation-widget-height", "--vscode-editor-dictation-widget-width", "--vscode-interactive-session-foreground", @@ -1014,6 +1018,8 @@ "--z-index-notebook-scrollbar", "--z-index-run-button-container", "--zoom-factor", - "--test-bar-width" + "--test-bar-width", + "--widget-color", + "--text-link-decoration" ] } diff --git a/build/lib/tsb/transpiler.js b/build/lib/tsb/transpiler.js index afec9062692..5dcc4ca1ed3 100644 --- a/build/lib/tsb/transpiler.js +++ b/build/lib/tsb/transpiler.js @@ -305,7 +305,7 @@ class SwcTranspiler { }, module: { type: 'amd', - noInterop: true + noInterop: false }, minify: false, }; @@ -313,7 +313,7 @@ class SwcTranspiler { ...this._swcrcAmd, module: { type: 'commonjs', - importInterop: 'none' + importInterop: 'swc' } }; static _swcrcEsm = { diff --git a/build/lib/tsb/transpiler.ts b/build/lib/tsb/transpiler.ts index a546ea63316..cbc3d9e8eee 100644 --- a/build/lib/tsb/transpiler.ts +++ b/build/lib/tsb/transpiler.ts @@ -388,7 +388,7 @@ export class SwcTranspiler implements ITranspiler { }, module: { type: 'amd', - noInterop: true + noInterop: false }, minify: false, }; @@ -397,7 +397,7 @@ export class SwcTranspiler implements ITranspiler { ...this._swcrcAmd, module: { type: 'commonjs', - importInterop: 'none' + importInterop: 'swc' } }; diff --git a/build/package.json b/build/package.json index 2b89bbc1c99..0bbeed3f136 100644 --- a/build/package.json +++ b/build/package.json @@ -4,7 +4,7 @@ "license": "MIT", "devDependencies": { "@azure/cosmos": "^3", - "@azure/identity": "^3.4.1", + "@azure/identity": "^4.2.1", "@azure/storage-blob": "^12.17.0", "@electron/get": "^2.0.0", "@types/ansi-colors": "^3.2.0", @@ -44,7 +44,6 @@ "esbuild": "0.20.0", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", - "gulp-shell": "^0.8.0", "jsonc-parser": "^2.3.0", "mime": "^1.4.1", "mkdirp": "^1.0.4", diff --git a/build/secrets/.secrets.baseline b/build/secrets/.secrets.baseline index 043a0dd264d..ced4e75271d 100644 --- a/build/secrets/.secrets.baseline +++ b/build/secrets/.secrets.baseline @@ -94,6 +94,10 @@ "path": "detect_secrets.filters.common.is_baseline_file", "filename": "build/secrets/.secrets.baseline" }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, { "path": "detect_secrets.filters.heuristic.is_indirect_reference" }, @@ -259,7 +263,7 @@ "filename": "build/azure-pipelines/sdl-scan.yml", "hashed_secret": "6bca595fb7e6690f8bacc9e0c1e056cd60e4b7cb", "is_verified": false, - "line_number": 67, + "line_number": 27, "is_secret": false } ], @@ -289,7 +293,8 @@ "filename": "build/secrets/README.md", "hashed_secret": "7585d1f7ceb90fd0b1ab42d0a6ca39fcf55065c7", "is_verified": false, - "line_number": 25 + "line_number": 25, + "is_secret": false } ], "cli/src/auth.rs": [ @@ -542,7 +547,7 @@ { "type": "Hex High Entropy String", "filename": "package.json", - "hashed_secret": "95b9187121d85faed7de90dc42b3036dd6bb3f9e", + "hashed_secret": "9a56939a8ba137e0755e5d00c65d907c28214797", "is_verified": false, "line_number": 4, "is_secret": false @@ -560,7 +565,7 @@ { "type": "Hex High Entropy String", "filename": "product.json", - "hashed_secret": "ba99905cb696b65a9547f0649bbecf3d808df947", + "hashed_secret": "089e9114f1162454281d07ef60b403a087b71f03", "is_verified": false, "line_number": 58, "is_secret": false @@ -720,6 +725,22 @@ "is_verified": false, "line_number": 19, "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts", + "hashed_secret": "4f8be53e6119f97cd67d38d5ce03aba6dae2316d", + "is_verified": false, + "line_number": 1827, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts", + "hashed_secret": "d84852bb1ee5e4a94dc72a90db9de265b7f18138", + "is_verified": false, + "line_number": 1832, + "is_secret": false } ], "src/vs/platform/backup/test/electron-main/backupMainService.test.ts": [ @@ -876,7 +897,7 @@ "filename": "src/vs/workbench/api/test/browser/extHostTelemetry.test.ts", "hashed_secret": "7fe54a7b420126077bba3453c0f9d31430735c5e", "is_verified": false, - "line_number": 255, + "line_number": 256, "is_secret": false } ], @@ -894,9 +915,9 @@ { "type": "Base64 High Entropy String", "filename": "src/vs/workbench/contrib/webview/browser/pre/index.html", - "hashed_secret": "558f38f9b8037c0fd72537287a216a3853c97872", + "hashed_secret": "d2ca05efa3869fb799c6229cc3c55a3505bb9c1c", "is_verified": false, - "line_number": 8, + "line_number": 9, "is_secret": false } ], @@ -910,16 +931,6 @@ "is_secret": false } ], - "src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts": [ - { - "type": "IBM Cloud IAM Key", - "filename": "src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts", - "hashed_secret": "0d43d6e259826e4ecbda1644424b26de54faa665", - "is_verified": false, - "line_number": 95, - "is_secret": false - } - ], "src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html": [ { "type": "Base64 High Entropy String", @@ -954,10 +965,10 @@ "filename": "test/integration/browser/src/index.ts", "hashed_secret": "3a8f125da17a2c187d430daf0045e0fdfa707bdd", "is_verified": false, - "line_number": 130, + "line_number": 134, "is_secret": false } ] }, - "generated_at": "2024-06-24T08:34:20Z" + "generated_at": "2024-07-16T00:32:17Z" } diff --git a/build/yarn.lock b/build/yarn.lock index 3131c43217c..d99ceffaadf 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -9,20 +9,19 @@ dependencies: tslib "^2.0.0" +"@azure/abort-controller@^2.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz#42fe0ccab23841d9905812c58f1082d27784566d" + integrity sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA== + dependencies: + tslib "^2.6.2" + "@azure/core-asynciterator-polyfill@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.0.tgz#dcccebb88406e5c76e0e1d52e8cc4c43a68b3ee7" integrity sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg== -"@azure/core-auth@^1.3.0": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.3.2.tgz#6a2c248576c26df365f6c7881ca04b7f6d08e3d0" - integrity sha512-7CU6DmCHIZp5ZPiZ9r3J17lTKMmYsm/zGvNkjArQwPkrLlZ1TZ+EUYfGgh2X31OLMVAQCTJZW4cXHJi02EbJnA== - dependencies: - "@azure/abort-controller" "^1.0.0" - tslib "^2.2.0" - -"@azure/core-auth@^1.5.0": +"@azure/core-auth@^1.3.0", "@azure/core-auth@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.5.0.tgz#a41848c5c31cb3b7c84c409885267d55a2c92e44" integrity sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw== @@ -81,22 +80,7 @@ dependencies: "@azure/core-asynciterator-polyfill" "^1.0.0" -"@azure/core-rest-pipeline@^1.1.0", "@azure/core-rest-pipeline@^1.2.0": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.3.2.tgz#82bfb4e960b4ecf4f1a1cdb1afde4ce9192aef09" - integrity sha512-kymICKESeHBpVLgQiAxllgWdSTopkqtmfPac8ITwMCxNEC6hzbSpqApYbjzxbBNkBMgoD4GESo6LLhR/sPh6kA== - dependencies: - "@azure/abort-controller" "^1.0.0" - "@azure/core-auth" "^1.3.0" - "@azure/core-tracing" "1.0.0-preview.13" - "@azure/logger" "^1.0.0" - form-data "^4.0.0" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - tslib "^2.2.0" - uuid "^8.3.0" - -"@azure/core-rest-pipeline@^1.5.0": +"@azure/core-rest-pipeline@^1.1.0", "@azure/core-rest-pipeline@^1.2.0", "@azure/core-rest-pipeline@^1.5.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.7.0.tgz#71f42c19af160422cc84513809ff9668d8047087" integrity sha512-e2awPzwMKHrmvYgZ0qIKNkqnCM1QoDs7A0rOiS3OSAlOQOz/kL7PPKHXwFMuBeaRvS8i7fgobJn79q2Cji5f+Q== @@ -126,21 +110,13 @@ dependencies: tslib "^2.2.0" -"@azure/core-util@^1.1.0", "@azure/core-util@^1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.6.1.tgz#fea221c4fa43c26543bccf799beb30c1c7878f5a" - integrity sha512-h5taHeySlsV9qxuK64KZxy4iln1BtMYlNt5jbuEFN3UFSAd1EwKg/Gjl5a6tZ/W8t6li3xPnutOx7zbDyXnPmQ== - dependencies: - "@azure/abort-controller" "^1.0.0" - tslib "^2.2.0" - -"@azure/core-util@^1.1.1": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.2.0.tgz#3499deba1fc36dda6f1912b791809b6f15d4a392" - integrity sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng== +"@azure/core-util@^1.1.0", "@azure/core-util@^1.1.1", "@azure/core-util@^1.3.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.9.0.tgz#469afd7e6452d5388b189f90d33f7756b0b210d1" + integrity sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw== dependencies: - "@azure/abort-controller" "^1.0.0" - tslib "^2.2.0" + "@azure/abort-controller" "^2.0.0" + tslib "^2.6.2" "@azure/cosmos@^3": version "3.17.3" @@ -161,20 +137,20 @@ universal-user-agent "^6.0.0" uuid "^8.3.0" -"@azure/identity@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-3.4.1.tgz#18ba48b7421c818ef8116e8eec3c03ec1a62649a" - integrity sha512-oQ/r5MBdfZTMIUcY5Ch8G7Vv9aIXDkEYyU4Dfqjim4MQN+LY2uiQ57P1JDopMLeHCsZxM4yy8lEdne3tM9Xhzg== +"@azure/identity@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-4.2.1.tgz#22b366201e989b7b41c0e1690e103bd579c31e4c" + integrity sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q== dependencies: "@azure/abort-controller" "^1.0.0" "@azure/core-auth" "^1.5.0" "@azure/core-client" "^1.4.0" "@azure/core-rest-pipeline" "^1.1.0" "@azure/core-tracing" "^1.0.0" - "@azure/core-util" "^1.6.1" + "@azure/core-util" "^1.3.0" "@azure/logger" "^1.0.0" - "@azure/msal-browser" "^3.5.0" - "@azure/msal-node" "^2.5.1" + "@azure/msal-browser" "^3.11.1" + "@azure/msal-node" "^2.9.2" events "^3.0.0" jws "^4.0.0" open "^8.0.0" @@ -188,24 +164,24 @@ dependencies: tslib "^2.0.0" -"@azure/msal-browser@^3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.5.0.tgz#eb64c931c78c2b75c70807f618e1284bbb183380" - integrity sha512-2NtMuel4CI3UEelCPKkNRXgKzpWEX48fvxIvPz7s0/sTcCaI08r05IOkH2GkXW+czUOtuY6+oGafJCpumnjRLg== +"@azure/msal-browser@^3.11.1": + version "3.17.0" + resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.17.0.tgz#dee9ccae586239e7e0708b261f7ffa5bc7e00fb7" + integrity sha512-csccKXmW2z7EkZ0I3yAoW/offQt+JECdTIV/KrnRoZyM7wCSsQWODpwod8ZhYy7iOyamcHApR9uCh0oD1M+0/A== dependencies: - "@azure/msal-common" "14.4.0" + "@azure/msal-common" "14.12.0" -"@azure/msal-common@14.4.0": - version "14.4.0" - resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.4.0.tgz#f938c1d96bb73d65baab985c96faaa273c97cfd5" - integrity sha512-ffCymScQuMKVj+YVfwNI52A5Tu+uiZO2eTf+c+3TXxdAssks4nokJhtr+uOOMxH0zDi6d1OjFKFKeXODK0YLSg== +"@azure/msal-common@14.12.0": + version "14.12.0" + resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.12.0.tgz#844abe269b071f8fa8949dadc2a7b65bbb147588" + integrity sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw== -"@azure/msal-node@^2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.5.1.tgz#d180a1ba5fdc611a318a8f018a2db3453e2e2898" - integrity sha512-PsPRISqCG253HQk1cAS7eJW7NWTbnBGpG+vcGGz5z4JYRdnM2EIXlj1aBpXCdozenEPtXEVvHn2ELleW1w82nQ== +"@azure/msal-node@^2.9.2": + version "2.9.2" + resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.9.2.tgz#e6d3c1661012c1bd0ef68e328f73a2fdede52931" + integrity sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ== dependencies: - "@azure/msal-common" "14.4.0" + "@azure/msal-common" "14.12.0" jsonwebtoken "^9.0.0" uuid "^8.3.0" @@ -743,13 +719,6 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - ansi-wrap@0.1.0, ansi-wrap@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" @@ -879,11 +848,11 @@ brace-expansion@^1.1.7: concat-map "0.0.1" braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" buffer-alloc-unsafe@^1.1.0: version "1.1.0" @@ -966,14 +935,6 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - cheerio-select@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" @@ -1077,23 +1038,11 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - color-support@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" @@ -1422,10 +1371,10 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -1617,28 +1566,11 @@ gulp-merge-json@^2.1.1: through "^2.3.8" vinyl "^2.1.0" -gulp-shell@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/gulp-shell/-/gulp-shell-0.8.0.tgz#0ed4980de1d0c67e5f6cce971d7201fd0be50555" - integrity sha512-wHNCgmqbWkk1c6Gc2dOL5SprcoeujQdeepICwfQRo91DIylTE7a794VEE+leq3cE2YDoiS5ulvRfKVIEMazcTQ== - dependencies: - chalk "^3.0.0" - fancy-log "^1.3.3" - lodash.template "^4.5.0" - plugin-error "^1.0.1" - through2 "^3.0.1" - tslib "^1.10.0" - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -1917,31 +1849,11 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" -lodash._reinterpolate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" - integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= - lodash.mergewith@^4.6.1: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== -lodash.template@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" - integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash.templatesettings "^4.0.0" - -lodash.templatesettings@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" - integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -2559,13 +2471,6 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - tar-fs@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" @@ -2657,20 +2562,10 @@ tree-sitter@^0.20.5, tree-sitter@^0.20.6: nan "^2.18.0" prebuild-install "^7.1.1" -tslib@^1.10.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" - integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== - -tslib@^2.2.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.0.0, tslib@^2.2.0, tslib@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== tunnel-agent@^0.6.0: version "0.6.0" diff --git a/cli/ThirdPartyNotices.txt b/cli/ThirdPartyNotices.txt index 7a74fb503c5..a9630c12022 100644 --- a/cli/ThirdPartyNotices.txt +++ b/cli/ThirdPartyNotices.txt @@ -1341,7 +1341,7 @@ SOFTWARE. --------------------------------------------------------- clap_derive 4.5.4 - MIT OR Apache-2.0 -https://github.com/clap-rs/clap/tree/master/clap_derive +https://github.com/clap-rs/clap Copyright (c) Individual contributors @@ -1367,7 +1367,7 @@ SOFTWARE. --------------------------------------------------------- clap_lex 0.7.0 - MIT OR Apache-2.0 -https://github.com/clap-rs/clap/tree/master/clap_lex +https://github.com/clap-rs/clap Copyright (c) Individual contributors @@ -3428,7 +3428,7 @@ DEALINGS IN THE SOFTWARE. httparse 1.8.0 - MIT/Apache-2.0 https://github.com/seanmonstar/httparse -Copyright (c) 2015-2021 Sean McArthur +Copyright (c) 2015-2024 Sean McArthur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -8507,6 +8507,7 @@ subtle 2.5.0 - BSD-3-Clause https://github.com/dalek-cryptography/subtle Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. +Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -10790,33 +10791,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI zbus 3.15.2 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -10824,33 +10799,7 @@ DEALINGS IN THE SOFTWARE. zbus_macros 3.15.2 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -10858,33 +10807,7 @@ DEALINGS IN THE SOFTWARE. zbus_names 2.6.1 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -10977,33 +10900,7 @@ licences; see files named LICENSE.*.txt for details. zvariant 3.15.2 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -11011,33 +10908,7 @@ DEALINGS IN THE SOFTWARE. zvariant_derive 3.15.2 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -11045,31 +10916,5 @@ DEALINGS IN THE SOFTWARE. zvariant_utils 1.0.1 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- \ No newline at end of file diff --git a/cli/src/async_pipe.rs b/cli/src/async_pipe.rs index e9b710c1d68..78aed6fe3e7 100644 --- a/cli/src/async_pipe.rs +++ b/cli/src/async_pipe.rs @@ -227,7 +227,7 @@ impl hyper::server::accept::Accept for PollableAsyncListener { } } -/// Gets a random name for a pipe/socket on the paltform +/// Gets a random name for a pipe/socket on the platform pub fn get_socket_name() -> PathBuf { cfg_if::cfg_if! { if #[cfg(unix)] { diff --git a/cli/src/auth.rs b/cli/src/auth.rs index 67f1bfa6bc7..2d9162c5483 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -287,7 +287,7 @@ impl StorageImplementation for ThreadKeyringStorage { #[derive(Default)] struct KeyringStorage { - // keywring storage can be split into multiple entries due to entry length limits + // keyring storage can be split into multiple entries due to entry length limits // on Windows https://github.com/microsoft/vscode-cli/issues/358 entries: Vec, } diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 79c4d3767a1..05e22e0cfb3 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -64,7 +64,7 @@ pub struct IntegratedCli { pub core: CliCore, } -/// Common CLI shared between intergated and standalone interfaces. +/// Common CLI shared between integrated and standalone interfaces. #[derive(Args, Debug, Default, Clone)] pub struct CliCore { /// One or more files, folders, or URIs to open. @@ -619,7 +619,7 @@ pub enum OutputFormat { #[derive(Args, Clone, Debug, Default)] pub struct ExistingTunnelArgs { /// Name you'd like to assign preexisting tunnel to use to connect the tunnel - /// Old option, new code sohuld just use `--name`. + /// Old option, new code should just use `--name`. #[clap(long, hide = true)] pub tunnel_name: Option, diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index fba92723426..12c0cdafec9 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -12,7 +12,6 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -use const_format::concatcp; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server}; use tokio::io::{AsyncBufReadExt, BufReader}; @@ -56,16 +55,9 @@ const RELEASE_CACHE_SECS: u64 = 60 * 60; /// Number of bytes for the secret keys. See workbench.ts for their usage. const SECRET_KEY_BYTES: usize = 32; /// Path to mint the key combining server and client parts. -const SECRET_KEY_MINT_PATH: &str = "/_vscode-cli/mint-key"; +const SECRET_KEY_MINT_PATH: &str = "_vscode-cli/mint-key"; /// Cookie set to the `SECRET_KEY_MINT_PATH` const PATH_COOKIE_NAME: &str = "vscode-secret-key-path"; -/// Cookie set to the `SECRET_KEY_MINT_PATH` -const PATH_COOKIE_VALUE: &str = concatcp!( - PATH_COOKIE_NAME, - "=", - SECRET_KEY_MINT_PATH, - "; SameSite=Strict; Path=/" -); /// HTTP-only cookie where the client's secret half is stored. const SECRET_KEY_COOKIE_NAME: &str = "vscode-cli-secret-half"; @@ -158,17 +150,22 @@ struct HandleContext { /// Handler function for an inbound request async fn handle(ctx: HandleContext, req: Request) -> Result, Infallible> { let client_key_half = get_client_key_half(&req); - let mut res = match req.uri().path() { - SECRET_KEY_MINT_PATH => handle_secret_mint(ctx, req), - _ => handle_proxied(ctx, req).await, + let path = req.uri().path(); + + let mut res = if path.starts_with(&ctx.cm.base_path) + && path.get(ctx.cm.base_path.len()..).unwrap_or_default() == SECRET_KEY_MINT_PATH + { + handle_secret_mint(&ctx, req) + } else { + handle_proxied(&ctx, req).await }; - append_secret_headers(&mut res, &client_key_half); + append_secret_headers(&ctx.cm.base_path, &mut res, &client_key_half); Ok(res) } -async fn handle_proxied(ctx: HandleContext, req: Request) -> Response { +async fn handle_proxied(ctx: &HandleContext, req: Request) -> Response { let release = if let Some((r, _)) = get_release_from_path(req.uri().path(), ctx.cm.platform) { r } else { @@ -194,7 +191,7 @@ async fn handle_proxied(ctx: HandleContext, req: Request) -> Response) -> Response { +fn handle_secret_mint(ctx: &HandleContext, req: Request) -> Response { use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); @@ -208,11 +205,20 @@ fn handle_secret_mint(ctx: HandleContext, req: Request) -> Response /// Appends headers to response to maintain the secret storage of the workbench: /// sets the `PATH_COOKIE_VALUE` so workbench.ts knows about the 'mint' endpoint, /// and maintains the http-only cookie the client will use for cookies. -fn append_secret_headers(res: &mut Response, client_key_half: &SecretKeyPart) { +fn append_secret_headers( + base_path: &str, + res: &mut Response, + client_key_half: &SecretKeyPart, +) { let headers = res.headers_mut(); headers.append( hyper::header::SET_COOKIE, - PATH_COOKIE_VALUE.parse().unwrap(), + format!( + "{}={}{}; SameSite=Strict; Path=/", + PATH_COOKIE_NAME, base_path, SECRET_KEY_MINT_PATH, + ) + .parse() + .unwrap(), ); headers.append( hyper::header::SET_COOKIE, @@ -496,6 +502,8 @@ struct ConnectionManager { pub platform: Platform, pub log: log::Logger, args: ServeWebArgs, + /// Server base path, ending in `/` + base_path: String, /// Cache where servers are stored cache: DownloadCache, /// Mapping of (Quality, Commit) to the state each server is in @@ -510,11 +518,24 @@ fn key_for_release(release: &Release) -> (Quality, String) { (release.quality, release.commit.clone()) } +fn normalize_base_path(p: &str) -> String { + let p = p.trim_matches('/'); + + if p.is_empty() { + return "/".to_string(); + } + + format!("/{}/", p.trim_matches('/')) +} + impl ConnectionManager { pub fn new(ctx: &CommandContext, platform: Platform, args: ServeWebArgs) -> Arc { + let base_path = normalize_base_path(args.server_base_path.as_deref().unwrap_or_default()); + Arc::new(Self { platform, args, + base_path, log: ctx.log.clone(), cache: DownloadCache::new(ctx.paths.web_server_storage()), update_service: UpdateService::new( diff --git a/cli/src/constants.rs b/cli/src/constants.rs index 6f604e8876e..1e277a89d6a 100644 --- a/cli/src/constants.rs +++ b/cli/src/constants.rs @@ -13,7 +13,7 @@ use crate::options::Quality; pub const CONTROL_PORT: u16 = 31545; -/// Protocol version sent to clients. This can be used to indiciate new or +/// Protocol version sent to clients. This can be used to indicate new or /// changed capabilities that clients may wish to leverage. /// 1 - Initial protocol version /// 2 - Addition of `serve.compressed` property to control whether servermsg's diff --git a/cli/src/tunnels/control_server.rs b/cli/src/tunnels/control_server.rs index f42984cfac1..dfb5e381179 100644 --- a/cli/src/tunnels/control_server.rs +++ b/cli/src/tunnels/control_server.rs @@ -920,9 +920,14 @@ async fn handle_update( info!(log, "Updating CLI to {}", latest_release); - updater + let r = updater .do_update(&latest_release, SilentCopyProgress()) - .await?; + .await; + + if let Err(e) = r { + did_update.store(false, Ordering::SeqCst); + return Err(e); + } Ok(UpdateResult { up_to_date: true, diff --git a/cli/src/tunnels/dev_tunnels.rs b/cli/src/tunnels/dev_tunnels.rs index 19ee3c2bf42..a964b446384 100644 --- a/cli/src/tunnels/dev_tunnels.rs +++ b/cli/src/tunnels/dev_tunnels.rs @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use super::protocol::{self, PortPrivacy}; +use super::protocol::{self, PortPrivacy, PortProtocol}; use crate::auth; use crate::constants::{IS_INTERACTIVE_CLI, PROTOCOL_VERSION_TAG, TUNNEL_SERVICE_USER_AGENT}; use crate::state::{LauncherPaths, PersistedState}; @@ -221,8 +221,11 @@ impl ActiveTunnel { &self, port_number: u16, privacy: PortPrivacy, + protocol: PortProtocol, ) -> Result<(), AnyError> { - self.manager.add_port_tcp(port_number, privacy).await?; + self.manager + .add_port_tcp(port_number, privacy, protocol) + .await?; Ok(()) } @@ -972,13 +975,14 @@ impl ActiveTunnelManager { &self, port_number: u16, privacy: PortPrivacy, + protocol: PortProtocol, ) -> Result<(), WrappedError> { self.relay .lock() .await .add_port(&TunnelPort { port_number, - protocol: Some(TUNNEL_PROTOCOL_AUTO.to_owned()), + protocol: Some(protocol.to_contract_str().to_string()), access_control: Some(privacy_to_tunnel_acl(privacy)), ..Default::default() }) diff --git a/cli/src/tunnels/local_forwarding.rs b/cli/src/tunnels/local_forwarding.rs index e6410860cb0..93c2d244159 100644 --- a/cli/src/tunnels/local_forwarding.rs +++ b/cli/src/tunnels/local_forwarding.rs @@ -27,7 +27,7 @@ use super::{ protocol::{ self, forward_singleton::{PortList, SetPortsResponse}, - PortPrivacy, + PortPrivacy, PortProtocol, }, shutdown_signal::ShutdownSignal, }; @@ -71,8 +71,13 @@ impl PortCount { } } } +#[derive(Clone)] +struct PortMapRec { + count: PortCount, + protocol: PortProtocol, +} -type PortMap = HashMap; +type PortMap = HashMap; /// The PortForwardingHandle is given out to multiple consumers to allow /// them to set_ports that they want to be forwarded. @@ -99,8 +104,8 @@ impl PortForwardingSender { for p in current.iter() { if !ports.contains(p) { let n = v.get_mut(&p.number).expect("expected port in map"); - n[p.privacy] -= 1; - if n.is_empty() { + n.count[p.privacy] -= 1; + if n.count.is_empty() { v.remove(&p.number); } } @@ -110,12 +115,19 @@ impl PortForwardingSender { if !current.contains(p) { match v.get_mut(&p.number) { Some(n) => { - n[p.privacy] += 1; + n.count[p.privacy] += 1; + n.protocol = p.protocol; } None => { - let mut pc = PortCount::default(); - pc[p.privacy] += 1; - v.insert(p.number, pc); + let mut count = PortCount::default(); + count[p.privacy] += 1; + v.insert( + p.number, + PortMapRec { + count, + protocol: p.protocol, + }, + ); } }; } @@ -164,22 +176,34 @@ impl PortForwardingReceiver { while self.receiver.changed().await.is_ok() { let next = self.receiver.borrow().clone(); - for (port, count) in current.iter() { - let privacy = count.primary_privacy(); - if !matches!(next.get(port), Some(n) if n.primary_privacy() == privacy) { + for (port, rec) in current.iter() { + let privacy = rec.count.primary_privacy(); + if !matches!(next.get(port), Some(n) if n.count.primary_privacy() == privacy) { match tunnel.remove_port(*port).await { - Ok(_) => info!(log, "stopped forwarding port {} at {:?}", *port, privacy), - Err(e) => error!(log, "failed to stop forwarding port {}: {}", port, e), + Ok(_) => info!( + log, + "stopped forwarding {} port {} at {:?}", rec.protocol, *port, privacy + ), + Err(e) => error!( + log, + "failed to stop forwarding {} port {}: {}", rec.protocol, port, e + ), } } } - for (port, count) in next.iter() { - let privacy = count.primary_privacy(); - if !matches!(current.get(port), Some(n) if n.primary_privacy() == privacy) { - match tunnel.add_port_tcp(*port, privacy).await { - Ok(_) => info!(log, "forwarding port {} at {:?}", port, privacy), - Err(e) => error!(log, "failed to forward port {}: {}", port, e), + for (port, rec) in next.iter() { + let privacy = rec.count.primary_privacy(); + if !matches!(current.get(port), Some(n) if n.count.primary_privacy() == privacy) { + match tunnel.add_port_tcp(*port, privacy, rec.protocol).await { + Ok(_) => info!( + log, + "forwarding {} port {} at {:?}", rec.protocol, port, privacy + ), + Err(e) => error!( + log, + "failed to forward {} port {}: {}", rec.protocol, port, e + ), } } } diff --git a/cli/src/tunnels/port_forwarder.rs b/cli/src/tunnels/port_forwarder.rs index 30267e8bc86..b05ae95ae40 100644 --- a/cli/src/tunnels/port_forwarder.rs +++ b/cli/src/tunnels/port_forwarder.rs @@ -12,7 +12,10 @@ use crate::{ util::errors::{AnyError, CannotForwardControlPort, ServerHasClosed}, }; -use super::{dev_tunnels::ActiveTunnel, protocol::PortPrivacy}; +use super::{ + dev_tunnels::ActiveTunnel, + protocol::{PortPrivacy, PortProtocol}, +}; pub enum PortForwardingRec { Forward(u16, PortPrivacy, oneshot::Sender>), @@ -89,7 +92,9 @@ impl PortForwardingProcessor { } if !self.forwarded.contains(&port) { - tunnel.add_port_tcp(port, privacy).await?; + tunnel + .add_port_tcp(port, privacy, PortProtocol::Auto) + .await?; self.forwarded.insert(port); } diff --git a/cli/src/tunnels/protocol.rs b/cli/src/tunnels/protocol.rs index d26ea978068..3654826c57e 100644 --- a/cli/src/tunnels/protocol.rs +++ b/cli/src/tunnels/protocol.rs @@ -299,10 +299,40 @@ pub enum PortPrivacy { Private, } +#[derive(Serialize, Deserialize, PartialEq, Copy, Eq, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum PortProtocol { + Auto, + Http, + Https, +} + +impl std::fmt::Display for PortProtocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_contract_str()) + } +} + +impl Default for PortProtocol { + fn default() -> Self { + Self::Auto + } +} + +impl PortProtocol { + pub fn to_contract_str(&self) -> &'static str { + match *self { + Self::Auto => tunnels::contracts::TUNNEL_PROTOCOL_AUTO, + Self::Http => tunnels::contracts::TUNNEL_PROTOCOL_HTTP, + Self::Https => tunnels::contracts::TUNNEL_PROTOCOL_HTTPS, + } + } +} + pub mod forward_singleton { use serde::{Deserialize, Serialize}; - use super::PortPrivacy; + use super::{PortPrivacy, PortProtocol}; pub const METHOD_SET_PORTS: &str = "set_ports"; @@ -310,6 +340,7 @@ pub mod forward_singleton { pub struct PortRec { pub number: u16, pub privacy: PortPrivacy, + pub protocol: PortProtocol, } pub type PortList = Vec; diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 8a00fda49b8..f6bfd751895 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -11,7 +11,9 @@ "icon": "images/icon.png", "activationEvents": [ "onProfile", - "onProfile:github" + "onProfile:github", + "onLanguage:json", + "onLanguage:jsonc" ], "enabledApiProposals": [ "profileContentHandlers" diff --git a/extensions/cpp/package.json b/extensions/cpp/package.json index c1d3f4882f6..9f3c890a48b 100644 --- a/extensions/cpp/package.json +++ b/extensions/cpp/package.json @@ -30,9 +30,13 @@ "id": "cpp", "extensions": [ ".cpp", + ".cppm", ".cc", + ".ccm", ".cxx", + ".cxxm", ".c++", + ".c++m", ".hpp", ".hh", ".hxx", diff --git a/extensions/css-language-features/client/src/browser/cssClientMain.ts b/extensions/css-language-features/client/src/browser/cssClientMain.ts index 6522c786389..c89997ffaa0 100644 --- a/extensions/css-language-features/client/src/browser/cssClientMain.ts +++ b/extensions/css-language-features/client/src/browser/cssClientMain.ts @@ -8,13 +8,6 @@ import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient import { startClient, LanguageClientConstructor } from '../cssClient'; import { LanguageClient } from 'vscode-languageclient/browser'; -declare const Worker: { - new(stringUrl: string): any; -}; -declare const TextDecoder: { - new(encoding?: string): { decode(buffer: ArrayBuffer): string }; -}; - let client: BaseLanguageClient | undefined; // this method is called when vs code is activated @@ -25,7 +18,7 @@ export async function activate(context: ExtensionContext) { worker.postMessage({ i10lLocation: l10n.uri?.toString(false) ?? '' }); const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - return new LanguageClient(id, name, clientOptions, worker); + return new LanguageClient(id, name, worker, clientOptions); }; client = await startClient(context, newLanguageClient, { TextDecoder }); diff --git a/extensions/css-language-features/client/tsconfig.json b/extensions/css-language-features/client/tsconfig.json index 573b24b4aa6..44b77895c10 100644 --- a/extensions/css-language-features/client/tsconfig.json +++ b/extensions/css-language-features/client/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./out" + "outDir": "./out", + "lib": [ + "webworker" + ] }, "include": [ "src/**/*", diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index f4f6adfb7f4..4ddfe8fce0d 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -994,7 +994,7 @@ ] }, "dependencies": { - "vscode-languageclient": "^10.0.0-next.5", + "vscode-languageclient": "^10.0.0-next.8", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 0f1750e800a..fe4f64d7c01 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -11,8 +11,8 @@ "browser": "./dist/browser/cssServerMain", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.2.14", - "vscode-languageserver": "^10.0.0-next.3", + "vscode-css-languageservice": "^6.3.0", + "vscode-languageserver": "^10.0.0-next.6", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index 8d4c46d641e..59033f770c1 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -24,28 +24,28 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-css-languageservice@^6.2.14: - version "6.2.14" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.2.14.tgz#d44fe75c03942d865a9c1a5ff5fb4e8dec1f89d0" - integrity sha512-5UPQ9Y1sUTnuMyaMBpO7LrBkqjhEJb5eAwdUlDp+Uez8lry+Tspnk3+3p2qWS4LlNsr4p3v9WkZxUf1ltgFpgw== +vscode-css-languageservice@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.3.0.tgz#51724d193d19b1a9075b1cef5cfeea6a555d2aa4" + integrity sha512-nU92imtkgzpCL0xikrIb8WvedV553F2BENzgz23wFuok/HLN5BeQmroMy26pUwFxV2eV8oNRmYCUv8iO7kSMhw== dependencies: "@vscode/l10n" "^0.0.18" vscode-languageserver-textdocument "^1.0.11" vscode-languageserver-types "3.17.5" vscode-uri "^3.0.8" -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageserver-protocol@3.17.6-next.4: - version "3.17.6-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.4.tgz#3c56f6eb588bb42fccc0ac54a0d5daf2d02f0a1b" - integrity sha512-/2bleKBxZLyRObS4mkpaWlVI9xGiUqMVmh/ztZ2vL4uP2XyIpraT45JBpn9AtXr0alqKJPKLuKr+/qcYULvm/w== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" vscode-languageserver-textdocument@^1.0.11: version "1.0.11" @@ -57,17 +57,17 @@ vscode-languageserver-types@3.17.5: resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== -vscode-languageserver@^10.0.0-next.3: - version "10.0.0-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.3.tgz#a63c5ea9fab1be93d7732ab0fdc18c9b37956e07" - integrity sha512-4x1qHImf6ePji4+8PX43lnBCBfBNdi2jneGX2k5FswJhx/cxaYYmusShmmtO/clyL1iurxJacrQoXfw9+ikhvg== +vscode-languageserver@^10.0.0-next.6: + version "10.0.0-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.6.tgz#0db118a93fe010c6b40cd04e91a15d09e7b60b60" + integrity sha512-0Lh1nhQfSxo5Ob+ayYO1QTIsDix2/Lc72Urm1KZrCFxK5zIFYaEh3QFeM9oZih4Rzs0ZkQPXXnoHtpvs5GT+Zw== dependencies: - vscode-languageserver-protocol "3.17.6-next.4" + vscode-languageserver-protocol "3.17.6-next.6" vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/css-language-features/yarn.lock b/extensions/css-language-features/yarn.lock index 25a22d07ca6..eef1c9ab57d 100644 --- a/extensions/css-language-features/yarn.lock +++ b/extensions/css-language-features/yarn.lock @@ -47,32 +47,32 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageclient@^10.0.0-next.5: - version "10.0.0-next.5" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.5.tgz#7431d88255a5fd99e9423659ac484b1f968200f3" - integrity sha512-JIf1WE7fvV0RElFM062bAummI433vcxuFwqoYAp+1zTVhta/jznxkTz1zs3Hbj2tiDfclf0TZ0qCxflAP1mY2Q== +vscode-languageclient@^10.0.0-next.8: + version "10.0.0-next.8" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.8.tgz#5afa0ced3b2ac68d31cc1c48edc4f289744542a0" + integrity sha512-D9inIHgqKayO9Tv0MeLb3XIL76yTuWmKdHqcGZKzjtQrMGJgASJDYWTapu+yAjEpDp0gmVOaCYyIlLB86ncDoQ== dependencies: minimatch "^9.0.3" semver "^7.6.0" - vscode-languageserver-protocol "3.17.6-next.4" + vscode-languageserver-protocol "3.17.6-next.6" -vscode-languageserver-protocol@3.17.6-next.4: - version "3.17.6-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.4.tgz#3c56f6eb588bb42fccc0ac54a0d5daf2d02f0a1b" - integrity sha512-/2bleKBxZLyRObS4mkpaWlVI9xGiUqMVmh/ztZ2vL4uP2XyIpraT45JBpn9AtXr0alqKJPKLuKr+/qcYULvm/w== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/extension-editing/src/extensionLinter.ts b/extensions/extension-editing/src/extensionLinter.ts index dd1727edb7b..b69dac0e2dd 100644 --- a/extensions/extension-editing/src/extensionLinter.ts +++ b/extensions/extension-editing/src/extensionLinter.ts @@ -149,7 +149,8 @@ export class ExtensionLinter { const effectiveProposalNames = extensionEnabledApiProposals[extensionId]; if (Array.isArray(effectiveProposalNames) && enabledApiProposals.children) { for (const child of enabledApiProposals.children) { - if (child.type === 'string' && !effectiveProposalNames.includes(getNodeValue(child))) { + const proposalName = child.type === 'string' ? getNodeValue(child) : undefined; + if (typeof proposalName === 'string' && !effectiveProposalNames.includes(proposalName.split('@')[0])) { const start = document.positionAt(child.offset); const end = document.positionAt(child.offset + child.length); diagnostics.push(new Diagnostic(new Range(start, end), apiProposalNotListed, DiagnosticSeverity.Error)); diff --git a/extensions/git/package.json b/extensions/git/package.json index dfbb29289db..4fc372e21f7 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1967,23 +1967,23 @@ { "command": "git.pushRef", "group": "navigation", - "when": "scmProvider == git && scmHistoryItemGroupHasUpstream" + "when": "scmProvider == git && scmHistoryItemGroupHasRemote" }, { "command": "git.publish", "group": "navigation", - "when": "scmProvider == git && !scmHistoryItemGroupHasUpstream" + "when": "scmProvider == git && !scmHistoryItemGroupHasRemote" } ], "scm/outgoingChanges/context": [ { "command": "git.pushRef", - "when": "scmProvider == git && scmHistoryItemGroupHasUpstream", + "when": "scmProvider == git && scmHistoryItemGroupHasRemote", "group": "1_modification@1" }, { "command": "git.publish", - "when": "scmProvider == git && !scmHistoryItemGroupHasUpstream", + "when": "scmProvider == git && !scmHistoryItemGroupHasRemote", "group": "1_modification@1" } ], @@ -3401,7 +3401,7 @@ "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", "file-type": "16.5.4", - "jschardet": "3.0.0", + "jschardet": "3.1.2", "picomatch": "2.3.1", "vscode-uri": "^2.0.0", "which": "4.0.0" diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index f049939c137..f94ecbab7b0 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -44,6 +44,7 @@ export class ApiRepositoryState implements RepositoryState { get mergeChanges(): Change[] { return this._repository.mergeGroup.resourceStates.map(r => new ApiChange(r)); } get indexChanges(): Change[] { return this._repository.indexGroup.resourceStates.map(r => new ApiChange(r)); } get workingTreeChanges(): Change[] { return this._repository.workingTreeGroup.resourceStates.map(r => new ApiChange(r)); } + get untrackedChanges(): Change[] { return this._repository.untrackedGroup.resourceStates.map(r => new ApiChange(r)); } readonly onDidChange: Event = this._repository.onDidRunGitStatus; diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 685b5413947..ce27e914244 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -122,6 +122,7 @@ export interface RepositoryState { readonly mergeChanges: Change[]; readonly indexChanges: Change[]; readonly workingTreeChanges: Change[]; + readonly untrackedChanges: Change[]; readonly onDidChange: Event; } @@ -144,6 +145,7 @@ export interface LogOptions { readonly sortByAuthorDate?: boolean; readonly shortStats?: boolean; readonly author?: string; + readonly refNames?: string[]; } export interface CommitOptions { diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 48f67f396d4..a5325033e77 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2068,69 +2068,71 @@ export class CommandCenter { let noStagedChanges = repository.indexGroup.resourceStates.length === 0; let noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; - if (promptToSaveFilesBeforeCommit !== 'never') { - let documents = workspace.textDocuments - .filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath)); - - if (promptToSaveFilesBeforeCommit === 'staged' || repository.indexGroup.resourceStates.length > 0) { - documents = documents - .filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath))); - } - - if (documents.length > 0) { - const message = documents.length === 1 - ? l10n.t('The following file has unsaved changes which won\'t be included in the commit if you proceed: {0}.\n\nWould you like to save it before committing?', path.basename(documents[0].uri.fsPath)) - : l10n.t('There are {0} unsaved files.\n\nWould you like to save them before committing?', documents.length); - const saveAndCommit = l10n.t('Save All & Commit Changes'); - const commit = l10n.t('Commit Changes'); - const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit); - - if (pick === saveAndCommit) { - await Promise.all(documents.map(d => d.save())); + if (!opts.empty) { + if (promptToSaveFilesBeforeCommit !== 'never') { + let documents = workspace.textDocuments + .filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath)); - // After saving the dirty documents, if there are any documents that are part of the - // index group we have to add them back in order for the saved changes to be committed + if (promptToSaveFilesBeforeCommit === 'staged' || repository.indexGroup.resourceStates.length > 0) { documents = documents .filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath))); - await repository.add(documents.map(d => d.uri)); + } - noStagedChanges = repository.indexGroup.resourceStates.length === 0; - noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; - } else if (pick !== commit) { - return; // do not commit on cancel + if (documents.length > 0) { + const message = documents.length === 1 + ? l10n.t('The following file has unsaved changes which won\'t be included in the commit if you proceed: {0}.\n\nWould you like to save it before committing?', path.basename(documents[0].uri.fsPath)) + : l10n.t('There are {0} unsaved files.\n\nWould you like to save them before committing?', documents.length); + const saveAndCommit = l10n.t('Save All & Commit Changes'); + const commit = l10n.t('Commit Changes'); + const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit); + + if (pick === saveAndCommit) { + await Promise.all(documents.map(d => d.save())); + + // After saving the dirty documents, if there are any documents that are part of the + // index group we have to add them back in order for the saved changes to be committed + documents = documents + .filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath))); + await repository.add(documents.map(d => d.uri)); + + noStagedChanges = repository.indexGroup.resourceStates.length === 0; + noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; + } else if (pick !== commit) { + return; // do not commit on cancel + } } } - } - // no changes, and the user has not configured to commit all in this case - if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit && !opts.empty && !opts.all) { - const suggestSmartCommit = config.get('suggestSmartCommit') === true; + // no changes, and the user has not configured to commit all in this case + if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit && !opts.all) { + const suggestSmartCommit = config.get('suggestSmartCommit') === true; - if (!suggestSmartCommit) { - return; - } + if (!suggestSmartCommit) { + return; + } - // prompt the user if we want to commit all or not - const message = l10n.t('There are no staged changes to commit.\n\nWould you like to stage all your changes and commit them directly?'); - const yes = l10n.t('Yes'); - const always = l10n.t('Always'); - const never = l10n.t('Never'); - const pick = await window.showWarningMessage(message, { modal: true }, yes, always, never); - - if (pick === always) { - config.update('enableSmartCommit', true, true); - } else if (pick === never) { - config.update('suggestSmartCommit', false, true); - return; - } else if (pick !== yes) { - return; // do not commit on cancel + // prompt the user if we want to commit all or not + const message = l10n.t('There are no staged changes to commit.\n\nWould you like to stage all your changes and commit them directly?'); + const yes = l10n.t('Yes'); + const always = l10n.t('Always'); + const never = l10n.t('Never'); + const pick = await window.showWarningMessage(message, { modal: true }, yes, always, never); + + if (pick === always) { + config.update('enableSmartCommit', true, true); + } else if (pick === never) { + config.update('suggestSmartCommit', false, true); + return; + } else if (pick !== yes) { + return; // do not commit on cancel + } } - } - if (opts.all === undefined) { - opts = { ...opts, all: noStagedChanges }; - } else if (!opts.all && noStagedChanges && !opts.empty) { - opts = { ...opts, all: true }; + if (opts.all === undefined) { + opts = { ...opts, all: noStagedChanges }; + } else if (!opts.all && noStagedChanges) { + opts = { ...opts, all: true }; + } } // enable signing of commits if configured diff --git a/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index 3f8553260e9..ace68c22524 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -220,16 +220,16 @@ class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider const historyProvider = this.repository.historyProvider; const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; - if (!currentHistoryItemGroup?.base) { + if (!currentHistoryItemGroup?.remote) { return []; } - const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base.id); + const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.remote.id); if (!ancestor) { return []; } - const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.base.id); + const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.remote.id); return changes; } catch (err) { return []; diff --git a/extensions/git/src/encoding.ts b/extensions/git/src/encoding.ts index a283f628594..c80fb6ee6d5 100644 --- a/extensions/git/src/encoding.ts +++ b/extensions/git/src/encoding.ts @@ -49,15 +49,38 @@ const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = { 'big5': 'cp950' }; -export function detectEncoding(buffer: Buffer): string | null { +const MAP_CANDIDATE_GUESS_ENCODING_TO_JSCHARDET: { [key: string]: string } = { + utf8: 'UTF-8', + utf16le: 'UTF-16LE', + utf16be: 'UTF-16BE', + windows1252: 'windows-1252', + windows1250: 'windows-1250', + iso88592: 'ISO-8859-2', + windows1251: 'windows-1251', + cp866: 'IBM866', + iso88595: 'ISO-8859-5', + koi8r: 'KOI8-R', + windows1253: 'windows-1253', + iso88597: 'ISO-8859-7', + windows1255: 'windows-1255', + iso88598: 'ISO-8859-8', + cp950: 'Big5', + shiftjis: 'SHIFT_JIS', + eucjp: 'EUC-JP', + euckr: 'EUC-KR', + gb2312: 'GB2312' +}; + +export function detectEncoding(buffer: Buffer, candidateGuessEncodings: string[]): string | null { const result = detectEncodingByBOM(buffer); if (result) { return result; } - const detected = jschardet.detect(buffer); + candidateGuessEncodings = candidateGuessEncodings.map(e => MAP_CANDIDATE_GUESS_ENCODING_TO_JSCHARDET[e]).filter(e => !!e); + const detected = jschardet.detect(buffer, candidateGuessEncodings.length > 0 ? { detectEncodings: candidateGuessEncodings } : undefined); if (!detected || !detected.encoding) { return null; } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 697e77815e4..0e2b37af5b7 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1165,6 +1165,12 @@ export class Repository { args.push(`--author="${options.author}"`); } + if (options?.refNames) { + args.push('--topo-order'); + args.push('--decorate=full'); + args.push(...options.refNames); + } + if (options?.path) { args.push('--', options.path); } @@ -1233,11 +1239,11 @@ export class Repository { .filter(entry => !!entry); } - async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise { + async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false, candidateGuessEncodings: string[] = []): Promise { const stdout = await this.buffer(object); if (autoGuessEncoding) { - encoding = detectEncoding(stdout) || encoding; + encoding = detectEncoding(stdout, candidateGuessEncodings) || encoding; } encoding = iconv.encodingExists(encoding) ? encoding : 'utf8'; @@ -1496,9 +1502,16 @@ export class Repository { return parseGitChanges(this.repositoryRoot, gitResult.stdout); } - async getMergeBase(ref1: string, ref2: string): Promise { + async getMergeBase(ref1: string, ref2: string, ...refs: string[]): Promise { try { - const args = ['merge-base', ref1, ref2]; + const args = ['merge-base']; + if (refs.length !== 0) { + args.push('--octopus'); + args.push(...refs); + } + + args.push(ref1, ref2); + const result = await this.exec(args); return result.stdout.trim(); diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index f238010e14c..22cbe9c493d 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel } from 'vscode'; +import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemLabel } from 'vscode'; import { Repository, Resource } from './repository'; import { IDisposable, dispose, filterEvent } from './util'; import { toGitUri } from './uri'; import { Branch, RefType, UpstreamRef } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Operation } from './operation'; +import { Commit } from './git'; export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable { @@ -21,6 +22,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; private _HEAD: Branch | undefined; + private _HEADMergeBase: Branch | undefined; + private _currentHistoryItemGroup: SourceControlHistoryItemGroup | undefined; get currentHistoryItemGroup(): SourceControlHistoryItemGroup | undefined { return this._currentHistoryItemGroup; } set currentHistoryItemGroup(value: SourceControlHistoryItemGroup | undefined) { @@ -29,6 +32,12 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } private historyItemDecorations = new Map(); + private historyItemLabels = new Map([ + ['HEAD -> refs/heads/', 'target'], + ['refs/heads/', 'git-branch'], + ['refs/remotes/', 'cloud'], + ['refs/tags/', 'tag'] + ]); private disposables: Disposable[] = []; @@ -43,18 +52,25 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this.logger.trace('GitHistoryProvider:onDidRunGitStatus - HEAD:', JSON.stringify(this._HEAD)); this.logger.trace('GitHistoryProvider:onDidRunGitStatus - repository.HEAD:', JSON.stringify(this.repository.HEAD)); + // Get the merge base of the current history item group + const mergeBase = await this.resolveHEADMergeBase(); + // Check if HEAD has changed if (!force && this._HEAD?.name === this.repository.HEAD?.name && this._HEAD?.commit === this.repository.HEAD?.commit && this._HEAD?.upstream?.name === this.repository.HEAD?.upstream?.name && this._HEAD?.upstream?.remote === this.repository.HEAD?.upstream?.remote && - this._HEAD?.upstream?.commit === this.repository.HEAD?.upstream?.commit) { + this._HEAD?.upstream?.commit === this.repository.HEAD?.upstream?.commit && + this._HEADMergeBase?.name === mergeBase?.name && + this._HEADMergeBase?.remote === mergeBase?.remote && + this._HEADMergeBase?.commit === mergeBase?.commit) { this.logger.trace('GitHistoryProvider:onDidRunGitStatus - HEAD has not changed'); return; } this._HEAD = this.repository.HEAD; + this._HEADMergeBase = mergeBase; // Check if HEAD does not support incoming/outgoing (detached commit, tag) if (!this.repository.HEAD?.name || !this.repository.HEAD?.commit || this.repository.HEAD.type === RefType.Tag) { @@ -67,11 +83,14 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this.currentHistoryItemGroup = { id: `refs/heads/${this.repository.HEAD.name ?? ''}`, name: this.repository.HEAD.name ?? '', - base: this.repository.HEAD.upstream ? - { - id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - } : undefined + remote: this.repository.HEAD.upstream ? { + id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + } : undefined, + base: mergeBase ? { + id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`, + name: `${mergeBase.remote}/${mergeBase.name}`, + } : undefined }; this.logger.trace(`GitHistoryProvider:onDidRunGitStatus - currentHistoryItemGroup (${force}): ${JSON.stringify(this.currentHistoryItemGroup)}`); @@ -112,6 +131,47 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return historyItems; } + async provideHistoryItems2(options: SourceControlHistoryOptions): Promise { + if (!this.currentHistoryItemGroup || !options.historyItemGroupIds) { + return []; + } + + // Deduplicate refNames + const refNames = Array.from(new Set(options.historyItemGroupIds)); + + // Get the merge base of the refNames + const refsMergeBase = await this.resolveHistoryItemGroupsMergeBase(refNames); + if (!refsMergeBase) { + return []; + } + + // Get the commits + const commits = await this.repository.log({ range: `${refsMergeBase}^..`, refNames }); + + await ensureEmojis(); + + const historyItems: SourceControlHistoryItem[] = []; + historyItems.push(...commits.map(commit => { + const newLineIndex = commit.message.indexOf('\n'); + const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message; + + const labels = this.resolveHistoryItemLabels(commit, refNames); + + return { + id: commit.hash, + parentIds: commit.parents, + message: emojify(subject), + author: commit.authorName, + icon: new ThemeIcon('git-commit'), + timestamp: commit.authorDate?.getTime(), + statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, + labels: labels.length !== 0 ? labels : undefined + }; + })); + + return historyItems; + } + async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise { if (!historyItemParentId) { const commit = await this.repository.getCommit(historyItemId); @@ -161,7 +221,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec async resolveHistoryItemGroupCommonAncestor(historyItemId1: string, historyItemId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined> { if (!historyItemId2) { - const upstreamRef = await this.resolveHistoryItemGroupBase(historyItemId1); + const upstreamRef = await this.resolveHistoryItemGroupMergeBase(historyItemId1); if (!upstreamRef) { this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupCommonAncestor - Failed to resolve history item group base for '${historyItemId1}'`); return undefined; @@ -191,7 +251,38 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return this.historyItemDecorations.get(uri.toString()); } - private async resolveHistoryItemGroupBase(historyItemId: string): Promise { + private async resolveHistoryItemGroupsMergeBase(refNames: string[]): Promise { + if (refNames.length < 2) { + return undefined; + } + + const refsMergeBase = await this.repository.getMergeBase(refNames[0], refNames[1], ...refNames.slice(2)); + return refsMergeBase; + } + + private resolveHistoryItemLabels(commit: Commit, refNames: string[]): SourceControlHistoryItemLabel[] { + const labels: SourceControlHistoryItemLabel[] = []; + + for (const label of commit.refNames) { + if (!label.startsWith('HEAD -> ') && !refNames.includes(label)) { + continue; + } + + for (const [key, value] of this.historyItemLabels) { + if (label.startsWith(key)) { + labels.push({ + title: label.substring(key.length), + icon: new ThemeIcon(value) + }); + break; + } + } + } + + return labels; + } + + private async resolveHistoryItemGroupMergeBase(historyItemId: string): Promise { try { // Upstream const branch = await this.repository.getBranch(historyItemId); @@ -202,7 +293,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // Base (config -> reflog -> default) const remoteBranch = await this.repository.getBranchBase(historyItemId); if (!remoteBranch?.remote || !remoteBranch?.name || !remoteBranch?.commit || remoteBranch?.type !== RefType.RemoteHead) { - this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupBase - Failed to resolve history item group base for '${historyItemId}'`); + this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupUpstreamOrBase - Failed to resolve history item group base for '${historyItemId}'`); return undefined; } @@ -213,12 +304,21 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec }; } catch (err) { - this.logger.error(`GitHistoryProvider:resolveHistoryItemGroupBase - Failed to get branch base for '${historyItemId}': ${err.message}`); + this.logger.error(`GitHistoryProvider:resolveHistoryItemGroupUpstreamOrBase - Failed to get branch base for '${historyItemId}': ${err.message}`); } return undefined; } + private async resolveHEADMergeBase(): Promise { + if (this.repository.HEAD?.type !== RefType.Head || !this.repository.HEAD?.name) { + return undefined; + } + + const mergeBase = await this.repository.getBranchBase(this.repository.HEAD.name); + return mergeBase; + } + dispose(): void { dispose(this.disposables); } diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index b7dc0fe35fb..cca0c84af1c 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -379,15 +379,26 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi while (foldersToTravers.length > 0) { const currentFolder = foldersToTravers.shift()!; + const children: fs.Dirent[] = []; + try { + children.push(...await fs.promises.readdir(currentFolder.path, { withFileTypes: true })); + + if (currentFolder.depth !== 0) { + result.push(currentFolder.path); + } + } + catch (err) { + this.logger.warn(`[swsf] Unable to read folder '${currentFolder.path}': ${err}`); + continue; + } + if (currentFolder.depth < maxDepth || maxDepth === -1) { - const children = await fs.promises.readdir(currentFolder.path, { withFileTypes: true }); const childrenFolders = children .filter(dirent => dirent.isDirectory() && dirent.name !== '.git' && !repositoryScanIgnoredFolders.find(f => pathEquals(dirent.name, f))) .map(dirent => path.join(currentFolder.path, dirent.name)); - result.push(...childrenFolders); foldersToTravers.push(...childrenFolders.map(folder => { return { path: folder, depth: currentFolder.depth + 1 }; })); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index ed959765a59..deec56f9eaa 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1112,8 +1112,8 @@ export class Repository implements Disposable { return this.run(Operation.Diff, () => this.repository.diffBetweenShortStat(ref1, ref2)); } - getMergeBase(ref1: string, ref2: string): Promise { - return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2)); + getMergeBase(ref1: string, ref2: string, ...refs: string[]): Promise { + return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2, ...refs)); } async hashObject(data: string): Promise { @@ -1865,13 +1865,14 @@ export class Repository implements Disposable { const configFiles = workspace.getConfiguration('files', Uri.file(filePath)); const defaultEncoding = configFiles.get('encoding'); const autoGuessEncoding = configFiles.get('autoGuessEncoding'); + const candidateGuessEncodings = configFiles.get('candidateGuessEncodings'); try { - return await this.repository.bufferString(`${ref}:${path}`, defaultEncoding, autoGuessEncoding); + return await this.repository.bufferString(`${ref}:${path}`, defaultEncoding, autoGuessEncoding, candidateGuessEncodings); } catch (err) { if (err.gitErrorCode === GitErrorCodes.WrongCase) { const gitRelativePath = await this.repository.getGitRelativePath(ref, path); - return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding); + return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding, candidateGuessEncodings); } throw err; diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index 266157e9e5c..06023a61227 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -182,10 +182,10 @@ isexe@^3.1.1: resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== -jschardet@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" - integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== +jschardet@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.1.2.tgz#9bf4364deba0677fe9e3bd9e29eda57febf2e9db" + integrity sha512-mw3CBZGzW8nUBPYhFU2ztZ/kJ6NClQUQVpyzvFMfznZsoC///ZQ30J2RCUanNsr5yF22LqhgYr/lj807/ZleWA== peek-readable@^4.1.0: version "4.1.0" diff --git a/extensions/go/cgmanifest.json b/extensions/go/cgmanifest.json index fc3c741c6f6..d27352e1339 100644 --- a/extensions/go/cgmanifest.json +++ b/extensions/go/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "go-syntax", "repositoryUrl": "https://github.com/worlpaker/go-syntax", - "commitHash": "254bd0f25182c86ffd2043824f8d003e11a34268" + "commitHash": "092c45ec9a51fe40188408d1371f123eaa4796fa" } }, "license": "MIT", "description": "The file syntaxes/go.tmLanguage.json is from https://github.com/worlpaker/go-syntax, which in turn was derived from https://github.com/jeff-hykin/better-go-syntax.", - "version": "0.6.6" + "version": "0.6.8" } ], "version": 1 diff --git a/extensions/go/syntaxes/go.tmLanguage.json b/extensions/go/syntaxes/go.tmLanguage.json index 083d4ffb1a4..21b370514d0 100644 --- a/extensions/go/syntaxes/go.tmLanguage.json +++ b/extensions/go/syntaxes/go.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/worlpaker/go-syntax/commit/254bd0f25182c86ffd2043824f8d003e11a34268", + "version": "https://github.com/worlpaker/go-syntax/commit/092c45ec9a51fe40188408d1371f123eaa4796fa", "name": "Go", "scopeName": "source.go", "patterns": [ @@ -32,6 +32,9 @@ }, { "include": "#group-variables" + }, + { + "include": "#field_hover" } ] }, @@ -2377,8 +2380,8 @@ }, { "comment": "make keyword", - "match": "(?:(\\bmake\\b)(?:(\\()((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+(?:\\([^\\)]+\\))?)?(?:[\\w\\.\\*\\[\\]\\{\\}]+)?(?:\\[(?:[^\\]]+)?\\])?(?:[\\w\\.\\*\\[\\]\\{\\}]+)?)?((?:\\,\\s*(?:[\\w\\.\\(\\)/\\+\\-\\<\\>\\&\\|\\%\\*]+)?)+)?(\\))))", - "captures": { + "begin": "(?:(\\bmake\\b)(?:(\\()((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+(?:\\([^\\)]+\\))?)?(?:[\\[\\]\\*]+)?(?:(?!\\bmap\\b)(?:[\\w\\.]+))?(\\[(?:(?:[\\S]+)(?:(?:\\,\\s*(?:[\\S]+))*))?\\])?(?:\\,)?)?))", + "beginCaptures": { "1": { "name": "entity.name.function.support.builtin.go" }, @@ -2398,18 +2401,19 @@ "name": "entity.name.type.go" } ] - }, - "4": { - "patterns": [ - { - "include": "$self" - } - ] - }, - "5": { + } + }, + "end": "\\)", + "endCaptures": { + "0": { "name": "punctuation.definition.end.bracket.round.go" } - } + }, + "patterns": [ + { + "include": "$self" + } + ] } ] }, @@ -2910,6 +2914,38 @@ } } }, + "field_hover": { + "comment": "struct field property and types when hovering with the mouse", + "match": "(?:(?<=^\\bfield\\b)\\s+([\\w\\*\\.]+)\\s+([\\s\\S]+))", + "captures": { + "1": { + "patterns": [ + { + "include": "#type-declarations" + }, + { + "match": "\\w+", + "name": "variable.other.property.go" + } + ] + }, + "2": { + "patterns": [ + { + "include": "#type-declarations" + }, + { + "match": "\\binvalid\\b\\s+\\btype\\b", + "name": "invalid.field.go" + }, + { + "match": "\\w+", + "name": "entity.name.type.go" + } + ] + } + } + }, "other_variables": { "comment": "all other variables", "match": "\\w+", diff --git a/extensions/html-language-features/client/src/browser/htmlClientMain.ts b/extensions/html-language-features/client/src/browser/htmlClientMain.ts index 3f10e6d131f..06997d39fb0 100644 --- a/extensions/html-language-features/client/src/browser/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/browser/htmlClientMain.ts @@ -8,13 +8,6 @@ import { LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient'; import { LanguageClient } from 'vscode-languageclient/browser'; -declare const Worker: { - new(stringUrl: string): any; -}; -declare const TextDecoder: { - new(encoding?: string): { decode(buffer: ArrayBuffer): string }; -}; - let client: AsyncDisposable | undefined; // this method is called when vs code is activated @@ -25,7 +18,7 @@ export async function activate(context: ExtensionContext) { worker.postMessage({ i10lLocation: l10n.uri?.toString(false) ?? '' }); const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - return new LanguageClient(id, name, clientOptions, worker); + return new LanguageClient(id, name, worker, clientOptions); }; const timer = { diff --git a/extensions/html-language-features/client/tsconfig.json b/extensions/html-language-features/client/tsconfig.json index 8f5cef74fd3..349af163eea 100644 --- a/extensions/html-language-features/client/tsconfig.json +++ b/extensions/html-language-features/client/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./out" + "outDir": "./out", + "lib": [ + "webworker" + ] }, "include": [ "src/**/*", diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 49489ff20df..ac026b973eb 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -259,7 +259,7 @@ }, "dependencies": { "@vscode/extension-telemetry": "^0.9.0", - "vscode-languageclient": "^10.0.0-next.3", + "vscode-languageclient": "^10.0.0-next.8", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 75bfa00de11..c1ddc242fa4 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -10,9 +10,9 @@ "main": "./out/node/htmlServerMain", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.2.13", - "vscode-html-languageservice": "^5.2.0", - "vscode-languageserver": "^10.0.0-next.2", + "vscode-css-languageservice": "^6.3.0", + "vscode-html-languageservice": "^5.3.0", + "vscode-languageserver": "^10.0.0-next.6", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index f327f1f352f..caaf929d895 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -24,38 +24,38 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-css-languageservice@^6.2.13: - version "6.2.13" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.2.13.tgz#c7c2dc7a081a203048d60157c65536767d6d96f8" - integrity sha512-2rKWXfH++Kxd9Z4QuEgd1IF7WmblWWU7DScuyf1YumoGLkY9DW6wF/OTlhOyO2rN63sWHX2dehIpKBbho4ZwvA== +vscode-css-languageservice@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.3.0.tgz#51724d193d19b1a9075b1cef5cfeea6a555d2aa4" + integrity sha512-nU92imtkgzpCL0xikrIb8WvedV553F2BENzgz23wFuok/HLN5BeQmroMy26pUwFxV2eV8oNRmYCUv8iO7kSMhw== dependencies: "@vscode/l10n" "^0.0.18" vscode-languageserver-textdocument "^1.0.11" vscode-languageserver-types "3.17.5" vscode-uri "^3.0.8" -vscode-html-languageservice@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-5.2.0.tgz#5b36f9131acc073cebaa2074dc8ff53e84c80f31" - integrity sha512-cdNMhyw57/SQzgUUGSIMQ66jikqEN6nBNyhx5YuOyj9310+eY9zw8Q0cXpiKzDX8aHYFewQEXRnigl06j/TVwQ== +vscode-html-languageservice@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-5.3.0.tgz#298ae5600c6749cbb95838975d07f449c44cb478" + integrity sha512-C4Z3KsP5Ih+fjHpiBc5jxmvCl+4iEwvXegIrzu2F5pktbWvQaBT3YkVPk8N+QlSSMk8oCG6PKtZ/Sq2YHb5e8g== dependencies: "@vscode/l10n" "^0.0.18" vscode-languageserver-textdocument "^1.0.11" vscode-languageserver-types "^3.17.5" vscode-uri "^3.0.8" -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageserver-protocol@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.3.tgz#09d3e28e9ad12270233d07fa0b69cf1d51d7dfe4" - integrity sha512-H8ATH5SAvc3JzttS+AL6g681PiBOZM/l34WP2JZk4akY3y7NqTP+f9cJ+MhrVBbD3aDS8bdAKewZgbFLW6M8Pg== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" vscode-languageserver-textdocument@^1.0.11: version "1.0.11" @@ -67,17 +67,17 @@ vscode-languageserver-types@3.17.5, vscode-languageserver-types@^3.17.5: resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== -vscode-languageserver@^10.0.0-next.2: - version "10.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.2.tgz#9a8ac58f72979961497c4fd7f6097561d4134d5f" - integrity sha512-WZdK/XO6EkNU6foYck49NpS35sahWhYFs4hwCGalH/6lhPmdUKABTnWioK/RLZKWqH8E5HdlAHQMfSBIxKBV9Q== +vscode-languageserver@^10.0.0-next.6: + version "10.0.0-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.6.tgz#0db118a93fe010c6b40cd04e91a15d09e7b60b60" + integrity sha512-0Lh1nhQfSxo5Ob+ayYO1QTIsDix2/Lc72Urm1KZrCFxK5zIFYaEh3QFeM9oZih4Rzs0ZkQPXXnoHtpvs5GT+Zw== dependencies: - vscode-languageserver-protocol "3.17.6-next.3" + vscode-languageserver-protocol "3.17.6-next.6" vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/html-language-features/yarn.lock b/extensions/html-language-features/yarn.lock index d1d73407809..aa2ea1c6840 100644 --- a/extensions/html-language-features/yarn.lock +++ b/extensions/html-language-features/yarn.lock @@ -149,32 +149,32 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageclient@^10.0.0-next.3: - version "10.0.0-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.3.tgz#d7336bafafb37569ac1d8e931d20ba2a6385cc64" - integrity sha512-jJhPdZaiELpPRnCUt8kQcF2HJuvzLgeW4HOGc6dp8Je+p08ndueVT4fpSsbly6KiEHr/Ri73tNz0CSfsOye6MA== +vscode-languageclient@^10.0.0-next.8: + version "10.0.0-next.8" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.8.tgz#5afa0ced3b2ac68d31cc1c48edc4f289744542a0" + integrity sha512-D9inIHgqKayO9Tv0MeLb3XIL76yTuWmKdHqcGZKzjtQrMGJgASJDYWTapu+yAjEpDp0gmVOaCYyIlLB86ncDoQ== dependencies: minimatch "^9.0.3" semver "^7.6.0" - vscode-languageserver-protocol "3.17.6-next.4" + vscode-languageserver-protocol "3.17.6-next.6" -vscode-languageserver-protocol@3.17.6-next.4: - version "3.17.6-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.4.tgz#3c56f6eb588bb42fccc0ac54a0d5daf2d02f0a1b" - integrity sha512-/2bleKBxZLyRObS4mkpaWlVI9xGiUqMVmh/ztZ2vL4uP2XyIpraT45JBpn9AtXr0alqKJPKLuKr+/qcYULvm/w== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index d923904ef9d..d881eb8ca22 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -15,7 +15,8 @@ ], "activationEvents": [ "onNotebook:jupyter-notebook", - "onNotebookSerializer:interactive" + "onNotebookSerializer:interactive", + "onNotebookSerializer:repl" ], "extensionKind": [ "workspace", @@ -61,6 +62,11 @@ "command": "notebook.cellOutput.copy", "title": "%copyCellOutput.title%", "category": "Notebook" + }, + { + "command": "notebook.cellOutput.openInTextEditor", + "title": "%openCellOutput.title%", + "category": "Notebook" } ], "notebooks": [ @@ -107,12 +113,24 @@ { "command": "notebook.cellOutput.copy", "when": "notebookCellHasOutputs" + }, + { + "command": "notebook.cellOutput.openInTextEditor", + "when": "false" } ], "webview/context": [ { "command": "notebook.cellOutput.copy", "when": "webviewId == 'notebook.output' && webviewSection == 'image'" + }, + { + "command": "notebook.cellOutput.copy", + "when": "webviewId == 'notebook.output' && webviewSection == 'text'" + }, + { + "command": "notebook.cellOutput.openInTextEditor", + "when": "webviewId == 'notebook.output' && webviewSection == 'text'" } ] } diff --git a/extensions/ipynb/package.nls.json b/extensions/ipynb/package.nls.json index af7d8f4ab47..7a3d95181cf 100644 --- a/extensions/ipynb/package.nls.json +++ b/extensions/ipynb/package.nls.json @@ -7,6 +7,7 @@ "openIpynbInNotebookEditor.title": "Open IPYNB File In Notebook Editor", "cleanInvalidImageAttachment.title": "Clean Invalid Image Attachment Reference", "copyCellOutput.title": "Copy Cell Output", + "openCellOutput.title": "Open Cell Output in Text Editor", "markdownAttachmentRenderer.displayName": { "message": "Markdown-It ipynb Cell Attachment renderer", "comment": [ diff --git a/extensions/ipynb/src/ipynbMain.ts b/extensions/ipynb/src/ipynbMain.ts index 0b403e6c94c..8720845de77 100644 --- a/extensions/ipynb/src/ipynbMain.ts +++ b/extensions/ipynb/src/ipynbMain.ts @@ -124,13 +124,6 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(cleaner); } - // Update new file contribution - vscode.extensions.onDidChange(() => { - vscode.commands.executeCommand('setContext', 'jupyterEnabled', vscode.extensions.getExtension('ms-toolsai.jupyter')); - }); - vscode.commands.executeCommand('setContext', 'jupyterEnabled', vscode.extensions.getExtension('ms-toolsai.jupyter')); - - return { get dropCustomMetadata() { return !useCustomPropertyInMetadata(); diff --git a/extensions/json-language-features/client/src/browser/jsonClientMain.ts b/extensions/json-language-features/client/src/browser/jsonClientMain.ts index f78f494d727..91ed937fe6f 100644 --- a/extensions/json-language-features/client/src/browser/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/browser/jsonClientMain.ts @@ -8,12 +8,6 @@ import { LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable, languageServerDescription } from '../jsonClient'; import { LanguageClient } from 'vscode-languageclient/browser'; -declare const Worker: { - new(stringUrl: string): any; -}; - -declare function fetch(uri: string, options: any): any; - let client: AsyncDisposable | undefined; // this method is called when vs code is activated @@ -24,7 +18,7 @@ export async function activate(context: ExtensionContext) { worker.postMessage({ i10lLocation: l10n.uri?.toString(false) ?? '' }); const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - return new LanguageClient(id, name, clientOptions, worker); + return new LanguageClient(id, name, worker, clientOptions); }; const schemaRequests: SchemaRequestService = { diff --git a/extensions/json-language-features/client/tsconfig.json b/extensions/json-language-features/client/tsconfig.json index aa51e4d0157..89e6a6c12b7 100644 --- a/extensions/json-language-features/client/tsconfig.json +++ b/extensions/json-language-features/client/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./out" + "outDir": "./out", + "lib": [ + "webworker" + ] }, "include": [ "src/**/*", diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index f86470429a4..fa8004e2b02 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -163,7 +163,7 @@ "dependencies": { "@vscode/extension-telemetry": "^0.9.0", "request-light": "^0.7.0", - "vscode-languageclient": "^10.0.0-next.5" + "vscode-languageclient": "^10.0.0-next.8" }, "devDependencies": { "@types/node": "20.x" diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 6134fb4224d..8472ca618a4 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -15,8 +15,8 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.2.1", "request-light": "^0.7.0", - "vscode-json-languageservice": "^5.3.11", - "vscode-languageserver": "^10.0.0-next.3", + "vscode-json-languageservice": "^5.4.0", + "vscode-languageserver": "^10.0.0-next.6", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 669e823497d..608619637e4 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -24,6 +24,11 @@ jsonc-parser@^3.2.1: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== +jsonc-parser@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.3.0.tgz#030d182672c8ffc2805db95467c83ffc0b033d9d" + integrity sha512-RK1Xb5alM78sdXpB2hqqK7jxAE5jTRH05GvUiLWqh7Vbp6OPHuJYlsAMRUDYNYJTAQgkmhHgkdwOEknxwP4ojQ== + request-light@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.7.0.tgz#885628bb2f8040c26401ebf258ec51c4ae98ac2a" @@ -34,51 +39,51 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-json-languageservice@^5.3.11: - version "5.3.11" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-5.3.11.tgz#71dbc56e9b1d07a57aa6a3d5569c8b7f2c05ca05" - integrity sha512-WYS72Ymria3dn8ZbjtBbt5K71m05wY1Q6hpXV5JxUT0q75Ts0ljLmnZJAVpx8DjPgYbFD+Z8KHpWh2laKLUCtQ== +vscode-json-languageservice@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-5.4.0.tgz#caf1aabc81b1df9faf6a97e4c34e13a2d10a8cdf" + integrity sha512-NCkkCr63OHVkE4lcb0xlUAaix6vE5gHQW4NrswbLEh3ArXj81lrGuFTsGEYEUXlNHdnc53vWPcjeSy/nMTrfXg== dependencies: "@vscode/l10n" "^0.0.18" - jsonc-parser "^3.2.1" + jsonc-parser "^3.3.0" vscode-languageserver-textdocument "^1.0.11" vscode-languageserver-types "^3.17.5" vscode-uri "^3.0.8" -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageserver-protocol@3.17.6-next.4: - version "3.17.6-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.4.tgz#3c56f6eb588bb42fccc0ac54a0d5daf2d02f0a1b" - integrity sha512-/2bleKBxZLyRObS4mkpaWlVI9xGiUqMVmh/ztZ2vL4uP2XyIpraT45JBpn9AtXr0alqKJPKLuKr+/qcYULvm/w== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" vscode-languageserver-textdocument@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA== -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== vscode-languageserver-types@^3.17.5: version "3.17.5" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== -vscode-languageserver@^10.0.0-next.3: - version "10.0.0-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.3.tgz#a63c5ea9fab1be93d7732ab0fdc18c9b37956e07" - integrity sha512-4x1qHImf6ePji4+8PX43lnBCBfBNdi2jneGX2k5FswJhx/cxaYYmusShmmtO/clyL1iurxJacrQoXfw9+ikhvg== +vscode-languageserver@^10.0.0-next.6: + version "10.0.0-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.6.tgz#0db118a93fe010c6b40cd04e91a15d09e7b60b60" + integrity sha512-0Lh1nhQfSxo5Ob+ayYO1QTIsDix2/Lc72Urm1KZrCFxK5zIFYaEh3QFeM9oZih4Rzs0ZkQPXXnoHtpvs5GT+Zw== dependencies: - vscode-languageserver-protocol "3.17.6-next.4" + vscode-languageserver-protocol "3.17.6-next.6" vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index b7ca937103a..c825de07683 100644 --- a/extensions/json-language-features/yarn.lock +++ b/extensions/json-language-features/yarn.lock @@ -154,32 +154,32 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageclient@^10.0.0-next.5: - version "10.0.0-next.5" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.5.tgz#7431d88255a5fd99e9423659ac484b1f968200f3" - integrity sha512-JIf1WE7fvV0RElFM062bAummI433vcxuFwqoYAp+1zTVhta/jznxkTz1zs3Hbj2tiDfclf0TZ0qCxflAP1mY2Q== +vscode-languageclient@^10.0.0-next.8: + version "10.0.0-next.8" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.8.tgz#5afa0ced3b2ac68d31cc1c48edc4f289744542a0" + integrity sha512-D9inIHgqKayO9Tv0MeLb3XIL76yTuWmKdHqcGZKzjtQrMGJgASJDYWTapu+yAjEpDp0gmVOaCYyIlLB86ncDoQ== dependencies: minimatch "^9.0.3" semver "^7.6.0" - vscode-languageserver-protocol "3.17.6-next.4" + vscode-languageserver-protocol "3.17.6-next.6" -vscode-languageserver-protocol@3.17.6-next.4: - version "3.17.6-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.4.tgz#3c56f6eb588bb42fccc0ac54a0d5daf2d02f0a1b" - integrity sha512-/2bleKBxZLyRObS4mkpaWlVI9xGiUqMVmh/ztZ2vL4uP2XyIpraT45JBpn9AtXr0alqKJPKLuKr+/qcYULvm/w== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== yallist@^4.0.0: version "4.0.0" diff --git a/extensions/latex/cgmanifest.json b/extensions/latex/cgmanifest.json index 609d875ac2f..b537c48ee8c 100644 --- a/extensions/latex/cgmanifest.json +++ b/extensions/latex/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "jlelong/vscode-latex-basics", "repositoryUrl": "https://github.com/jlelong/vscode-latex-basics", - "commitHash": "56e2dc967e6bafafc1acfeeb80af42b8328b021a" + "commitHash": "5d7c2a4e451a932b776f6d9342087be6a1e8c0a1" } }, "license": "MIT", - "version": "1.7.0", + "version": "1.9.0", "description": "The files in syntaxes/ were originally part of https://github.com/James-Yu/LaTeX-Workshop. They have been extracted in the hope that they can useful outside of the LaTeX-Workshop extension.", "licenseDetail": [ "Copyright (c) vscode-latex-basics authors", diff --git a/extensions/latex/syntaxes/TeX.tmLanguage.json b/extensions/latex/syntaxes/TeX.tmLanguage.json index 205d8bdfce0..0cb03e61466 100644 --- a/extensions/latex/syntaxes/TeX.tmLanguage.json +++ b/extensions/latex/syntaxes/TeX.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jlelong/vscode-latex-basics/commit/8624d0bdae950a70cdf4a1c3d19c7398ef851721", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/5d7c2a4e451a932b776f6d9342087be6a1e8c0a1", "name": "TeX", "scopeName": "text.tex", "patterns": [ @@ -108,7 +108,25 @@ "name": "punctuation.definition.function.tex" } }, - "match": "(\\\\)(?:[,;]|(?:[\\p{Alphabetic}@]+(?:(?:_[\\p{Alphabetic}@]+)*:[NncVvoxefTFpwD]*)?))", + "match": "(\\\\)_*[\\p{Alphabetic}@]+(?:_[\\p{Alphabetic}@]+)*:[NncVvoxefTFpwD]*", + "name": "support.class.general.latex3.tex" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.function.tex" + } + }, + "match": "(\\.)[\\p{Alphabetic}@]+(?:_[\\p{Alphabetic}@]+)*:[NncVvoxefTFpwD]*", + "name": "support.class.general.latex3.tex" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.function.tex" + } + }, + "match": "(\\\\)(?:[,;]|(?:[\\p{Alphabetic}@]+))", "name": "support.function.general.tex" }, { diff --git a/extensions/markdown-basics/language-configuration.json b/extensions/markdown-basics/language-configuration.json index f1e7859ccca..6e1766db02c 100644 --- a/extensions/markdown-basics/language-configuration.json +++ b/extensions/markdown-basics/language-configuration.json @@ -79,6 +79,10 @@ [ "<", ">" + ], + [ + "~", + "~" ] ], "folding": { diff --git a/extensions/markdown-language-features/media/markdown.css b/extensions/markdown-language-features/media/markdown.css index 168f6a8a862..800be985a43 100644 --- a/extensions/markdown-language-features/media/markdown.css +++ b/extensions/markdown-language-features/media/markdown.css @@ -205,7 +205,7 @@ table > tbody > tr + tr > td { blockquote { margin: 0; - padding: 2px 16px 0 10px; + padding: 0px 16px 0 10px; border-left-width: 5px; border-left-style: solid; border-radius: 2px; diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 3d73a7621fd..fd3ba077028 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -14,7 +14,8 @@ ], "activationEvents": [], "enabledApiProposals": [ - "idToken" + "idToken", + "authGetSessions" ], "capabilities": { "virtualWorkspaces": true, diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index df36686dc9a..bc4d71e56d6 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -203,11 +203,13 @@ export class AzureActiveDirectoryService { return this._sessionChangeEmitter.event; } - public getSessions(scopes?: string[]): Promise { + public getSessions(scopes?: string[], account?: vscode.AuthenticationSessionAccountInformation): Promise { if (!scopes) { this._logger.info('Getting sessions for all scopes...'); - const sessions = this._tokens.map(token => this.convertToSessionSync(token)); - this._logger.info(`Got ${sessions.length} sessions for all scopes...`); + const sessions = this._tokens + .filter(token => !account?.label || token.account.label === account.label) + .map(token => this.convertToSessionSync(token)); + this._logger.info(`Got ${sessions.length} sessions for all scopes${account ? ` for account '${account.label}'` : ''}...`); return Promise.resolve(sessions); } @@ -238,23 +240,43 @@ export class AzureActiveDirectoryService { tenant: this.getTenantId(scopes), }; - this._logger.trace(`[${scopeData.scopeStr}] Queued getting sessions`); - return this._sequencer.queue(modifiedScopesStr, () => this.doGetSessions(scopeData)); + this._logger.trace(`[${scopeData.scopeStr}] Queued getting sessions` + account ? ` for ${account?.label}` : ''); + return this._sequencer.queue(modifiedScopesStr, () => this.doGetSessions(scopeData, account)); } - private async doGetSessions(scopeData: IScopeData): Promise { - this._logger.info(`[${scopeData.scopeStr}] Getting sessions`); + private async doGetSessions(scopeData: IScopeData, account?: vscode.AuthenticationSessionAccountInformation): Promise { + this._logger.info(`[${scopeData.scopeStr}] Getting sessions` + account ? ` for ${account?.label}` : ''); - const matchingTokens = this._tokens.filter(token => token.scope === scopeData.scopeStr); + const matchingTokens = this._tokens + .filter(token => token.scope === scopeData.scopeStr) + .filter(token => !account?.label || token.account.label === account.label); // If we still don't have a matching token try to get a new token from an existing token by using // the refreshToken. This is documented here: // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#refresh-the-access-token // "Refresh tokens are valid for all permissions that your client has already received consent for." if (!matchingTokens.length) { - // Get a token with the correct client id. - const token = scopeData.clientId === DEFAULT_CLIENT_ID - ? this._tokens.find(t => t.refreshToken && !t.scope.includes('VSCODE_CLIENT_ID')) - : this._tokens.find(t => t.refreshToken && t.scope.includes(`VSCODE_CLIENT_ID:${scopeData.clientId}`)); + // Get a token with the correct client id and account. + let token: IToken | undefined; + for (const t of this._tokens) { + // No refresh token, so we can't make a new token from this session + if (!t.refreshToken) { + continue; + } + // Need to make sure the account matches if we were provided one + if (account?.label && t.account.label !== account.label) { + continue; + } + // If the client id is the default client id, then check for the absence of the VSCODE_CLIENT_ID scope + if (scopeData.clientId === DEFAULT_CLIENT_ID && !t.scope.includes('VSCODE_CLIENT_ID')) { + token = t; + break; + } + // If the client id is not the default client id, then check for the matching VSCODE_CLIENT_ID scope + if (scopeData.clientId !== DEFAULT_CLIENT_ID && t.scope.includes(`VSCODE_CLIENT_ID:${scopeData.clientId}`)) { + token = t; + break; + } + } if (token) { this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Found a matching token with a different scopes '${token.scope}'. Attempting to get a new session using the existing session.`); @@ -275,7 +297,7 @@ export class AzureActiveDirectoryService { .map(result => (result as PromiseFulfilledResult).value); } - public createSession(scopes: string[]): Promise { + public createSession(scopes: string[], account?: vscode.AuthenticationSessionAccountInformation): Promise { let modifiedScopes = [...scopes]; if (!modifiedScopes.includes('openid')) { modifiedScopes.push('openid'); @@ -301,11 +323,11 @@ export class AzureActiveDirectoryService { }; this._logger.trace(`[${scopeData.scopeStr}] Queued creating session`); - return this._sequencer.queue(scopeData.scopeStr, () => this.doCreateSession(scopeData)); + return this._sequencer.queue(scopeData.scopeStr, () => this.doCreateSession(scopeData, account)); } - private async doCreateSession(scopeData: IScopeData): Promise { - this._logger.info(`[${scopeData.scopeStr}] Creating session`); + private async doCreateSession(scopeData: IScopeData, account?: vscode.AuthenticationSessionAccountInformation): Promise { + this._logger.info(`[${scopeData.scopeStr}] Creating session` + account ? ` for ${account?.label}` : ''); const runsRemote = vscode.env.remoteName !== undefined; const runsServerless = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Web; @@ -316,17 +338,17 @@ export class AzureActiveDirectoryService { return await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Signing in to your account...'), cancellable: true }, async (_progress, token) => { if (runsRemote || runsServerless) { - return await this.createSessionWithoutLocalServer(scopeData, token); + return await this.createSessionWithoutLocalServer(scopeData, account?.label, token); } try { - return await this.createSessionWithLocalServer(scopeData, token); + return await this.createSessionWithLocalServer(scopeData, account?.label, token); } catch (e) { this._logger.error(`[${scopeData.scopeStr}] Error creating session: ${e}`); // If the error was about starting the server, try directly hitting the login endpoint instead if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') { - return this.createSessionWithoutLocalServer(scopeData, token); + return this.createSessionWithoutLocalServer(scopeData, account?.label, token); } throw e; @@ -334,7 +356,7 @@ export class AzureActiveDirectoryService { }); } - private async createSessionWithLocalServer(scopeData: IScopeData, token: vscode.CancellationToken): Promise { + private async createSessionWithLocalServer(scopeData: IScopeData, loginHint: string | undefined, token: vscode.CancellationToken): Promise { this._logger.trace(`[${scopeData.scopeStr}] Starting login flow with local server`); const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); @@ -344,11 +366,15 @@ export class AzureActiveDirectoryService { client_id: scopeData.clientId, redirect_uri: redirectUrl, scope: scopeData.scopesToSend, - prompt: 'select_account', code_challenge_method: 'S256', code_challenge: codeChallenge, - }).toString(); - const loginUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize?${qs}`, this._env.activeDirectoryEndpointUrl).toString(); + }); + if (loginHint) { + qs.set('login_hint', loginHint); + } else { + qs.set('prompt', 'select_account'); + } + const loginUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize?${qs.toString()}`, this._env.activeDirectoryEndpointUrl).toString(); const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl); await server.start(); @@ -370,7 +396,7 @@ export class AzureActiveDirectoryService { return session; } - private async createSessionWithoutLocalServer(scopeData: IScopeData, token: vscode.CancellationToken): Promise { + private async createSessionWithoutLocalServer(scopeData: IScopeData, loginHint: string | undefined, token: vscode.CancellationToken): Promise { this._logger.trace(`[${scopeData.scopeStr}] Starting login flow without local server`); let callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`)); const nonce = generateCodeVerifier(); @@ -383,17 +409,22 @@ export class AzureActiveDirectoryService { const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); const signInUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize`, this._env.activeDirectoryEndpointUrl); - signInUrl.search = new URLSearchParams({ + const qs = new URLSearchParams({ response_type: 'code', client_id: encodeURIComponent(scopeData.clientId), response_mode: 'query', redirect_uri: redirectUrl, state, scope: scopeData.scopesToSend, - prompt: 'select_account', code_challenge_method: 'S256', code_challenge: codeChallenge, - }).toString(); + }); + if (loginHint) { + qs.append('login_hint', loginHint); + } else { + qs.append('prompt', 'select_account'); + } + signInUrl.search = qs.toString(); const uri = vscode.Uri.parse(signInUrl.toString()); vscode.env.openExternal(uri); diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 02cfb4643f4..87dc94e4c25 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -123,8 +123,8 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('microsoft', 'Microsoft', { onDidChangeSessions: loginService.onDidChangeSessions, - getSessions: (scopes: string[]) => loginService.getSessions(scopes), - createSession: async (scopes: string[]) => { + getSessions: (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => loginService.getSessions(scopes, options?.account), + createSession: async (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => { try { /* __GDPR__ "login" : { @@ -138,7 +138,7 @@ export async function activate(context: vscode.ExtensionContext) { scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), }); - return await loginService.createSession(scopes); + return await loginService.createSession(scopes, options?.account); } catch (e) { /* __GDPR__ "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } diff --git a/extensions/microsoft-authentication/tsconfig.json b/extensions/microsoft-authentication/tsconfig.json index 4b9d06d1847..cad76d078bd 100644 --- a/extensions/microsoft-authentication/tsconfig.json +++ b/extensions/microsoft-authentication/tsconfig.json @@ -22,6 +22,7 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.idToken.d.ts" + "../../src/vscode-dts/vscode.proposed.idToken.d.ts", + "../../src/vscode-dts/vscode.proposed.authGetSessions.d.ts" ] } diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index 8954017fffc..8f5fa908cb9 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -11,7 +11,7 @@ import { formatStackTrace } from './stackTraceHelper'; function clearContainer(container: HTMLElement) { while (container.firstChild) { - container.removeChild(container.firstChild); + container.firstChild.remove(); } } @@ -378,7 +378,7 @@ function renderStream(outputInfo: OutputWithAppend, outputElement: HTMLElement, contentParent = document.createElement('div'); contentParent.appendChild(newContent); while (outputElement.firstChild) { - outputElement.removeChild(outputElement.firstChild); + outputElement.firstChild.remove(); } outputElement.appendChild(contentParent); } @@ -462,7 +462,7 @@ export const activate: ActivationFunction = (ctx) => { border-color: var(--theme-input-focus-border-color); } #container div.output .scrollable { - overflow-y: scroll; + overflow-y: auto; max-height: var(--notebook-cell-output-max-height); } #container div.output .scrollable.scrollbar-visible { diff --git a/extensions/notebook-renderers/src/textHelper.ts b/extensions/notebook-renderers/src/textHelper.ts index b49dbb6ad8d..9c080c7f9e4 100644 --- a/extensions/notebook-renderers/src/textHelper.ts +++ b/extensions/notebook-renderers/src/textHelper.ts @@ -71,6 +71,11 @@ function generateNestedViewAllElement(outputId: string) { function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number, linkOptions: LinkOptions) { const container = document.createElement('div'); + container.setAttribute('data-vscode-context', JSON.stringify({ + webviewSection: 'text', + outputId: id, + 'preventDefaultContextMenuItems': true + })); const lineCount = buffer.length; if (lineCount <= linesLimit) { @@ -95,6 +100,11 @@ function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number function scrollableArrayOfString(id: string, buffer: string[], linkOptions: LinkOptions) { const element = document.createElement('div'); + element.setAttribute('data-vscode-context', JSON.stringify({ + webviewSection: 'text', + outputId: id, + 'preventDefaultContextMenuItems': true + })); if (buffer.length > softScrollableLineLimit) { element.appendChild(generateNestedViewAllElement(id)); } diff --git a/extensions/notebook-renderers/yarn.lock b/extensions/notebook-renderers/yarn.lock index 3cbe531e0fd..00c3e704dba 100644 --- a/extensions/notebook-renderers/yarn.lock +++ b/extensions/notebook-renderers/yarn.lock @@ -408,9 +408,9 @@ word-wrap@~1.2.3: integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== ws@^8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xml-name-validator@^4.0.0: version "4.0.0" diff --git a/extensions/npm/yarn.lock b/extensions/npm/yarn.lock index a7afc9f801f..be4b192c67d 100644 --- a/extensions/npm/yarn.lock +++ b/extensions/npm/yarn.lock @@ -39,21 +39,21 @@ brace-expansion@^2.0.1: balanced-match "^1.0.0" braces@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" diff --git a/extensions/package.json b/extensions/package.json index 2c83af40936..940bbe9b8a2 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "5.4.5" + "typescript": "^5.5.2" }, "scripts": { "postinstall": "node ./postinstall.mjs" diff --git a/extensions/python/cgmanifest.json b/extensions/python/cgmanifest.json index ace7056c995..37a21b2de54 100644 --- a/extensions/python/cgmanifest.json +++ b/extensions/python/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "MagicStack/MagicPython", "repositoryUrl": "https://github.com/MagicStack/MagicPython", - "commitHash": "7d0f2b22a5ad8fccbd7341bc7b7a715169283044" + "commitHash": "c9b3409deb69acec31bbf7913830e93a046b30cc" } }, "license": "MIT", diff --git a/extensions/tunnel-forwarding/src/extension.ts b/extensions/tunnel-forwarding/src/extension.ts index 3dc88224aaa..299c728719f 100644 --- a/extensions/tunnel-forwarding/src/extension.ts +++ b/extensions/tunnel-forwarding/src/extension.ts @@ -37,6 +37,7 @@ class Tunnel implements vscode.Tunnel { constructor( public readonly remoteAddress: { port: number; host: string }, public readonly privacy: TunnelPrivacyId, + public readonly protocol: 'http' | 'https', ) { } public setPortFormat(formatString: string) { @@ -82,7 +83,7 @@ export async function activate(context: vscode.ExtensionContext) { { tunnelFeatures: { elevation: false, - protocol: false, + protocol: true, privacyOptions: [ { themeIcon: 'globe', id: TunnelPrivacyId.Public, label: vscode.l10n.t('Public') }, { themeIcon: 'lock', id: TunnelPrivacyId.Private, label: vscode.l10n.t('Private') }, @@ -152,6 +153,7 @@ class TunnelProvider implements vscode.TunnelProvider { const tunnel = new Tunnel( tunnelOptions.remoteAddress, (tunnelOptions.privacy as TunnelPrivacyId) || TunnelPrivacyId.Private, + tunnelOptions.protocol === 'https' ? 'https' : 'http', ); this.tunnels.add(tunnel); @@ -238,7 +240,7 @@ class TunnelProvider implements vscode.TunnelProvider { return; } - const ports = [...this.tunnels].map(t => ({ number: t.remoteAddress.port, privacy: t.privacy })); + const ports = [...this.tunnels].map(t => ({ number: t.remoteAddress.port, privacy: t.privacy, protocol: t.protocol })); this.state.process.stdin.write(`${JSON.stringify(ports)}\n`); if (ports.length === 0 && !this.state.cleanupTimeout) { diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 8d651f60a93..b41426a1c59 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -1324,15 +1324,21 @@ "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", "scope": "window" }, + "typescript.tsserver.enableRegionDiagnostics": { + "type": "boolean", + "default": true, + "description": "%typescript.tsserver.enableRegionDiagnostics%", + "scope": "window" + }, "javascript.experimental.updateImportsOnPaste": { - "scope": "resource", + "scope": "window", "type": "boolean", "default": false, "description": "%configuration.updateImportsOnPaste%", "tags": ["experimental"] }, "typescript.experimental.updateImportsOnPaste": { - "scope": "resource", + "scope": "window", "type": "boolean", "default": false, "description": "%configuration.updateImportsOnPaste%", diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 4f276905d96..cba24007314 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -16,6 +16,7 @@ "typescript.tsserver.pluginPaths": "Additional paths to discover TypeScript Language Service plugins.", "typescript.tsserver.pluginPaths.item": "Either an absolute or relative path. Relative path will be resolved against workspace folder(s).", "typescript.tsserver.trace": "Enables tracing of messages sent to the TS server. This trace can be used to diagnose TS Server issues. The trace may contain file paths, source code, and other potentially sensitive information from your project.", + "typescript.tsserver.enableRegionDiagnostics": "Enables region-based diagnostics in TypeScript. Requires using TypeScript 5.6+ in the workspace.", "typescript.validate.enable": "Enable/disable TypeScript validation.", "typescript.format.enable": "Enable/disable default TypeScript formatter.", "javascript.format.enable": "Enable/disable default JavaScript formatter.", @@ -219,7 +220,7 @@ "configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors on web even when project wide IntelliSense is enabled. This is always on when project wide IntelliSense is not enabled or available. See `#typescript.tsserver.web.projectWideIntellisense.enabled#`", "configuration.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web. This enables IntelliSense for imported packages. Requires `#typescript.tsserver.web.projectWideIntellisense.enabled#`. Currently not supported for Safari.", "configuration.tsserver.nodePath": "Run TS Server on a custom Node installation. This can be a path to a Node executable, or 'node' if you want VS Code to detect a Node installation.", - "configuration.updateImportsOnPaste": "Automatically update imports when pasting code. Requires TypeScript 5.5+.", + "configuration.updateImportsOnPaste": "Automatically update imports when pasting code. Requires TypeScript 5.6+.", "walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js", "walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.", "walkthroughs.nodejsWelcome.downloadNode.forMacOrWindows.title": "Install Node.js", diff --git a/extensions/typescript-language-features/src/configuration/configuration.ts b/extensions/typescript-language-features/src/configuration/configuration.ts index a08ca921e0c..639f3d346e0 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.ts @@ -124,6 +124,7 @@ export interface TypeScriptServiceConfiguration { readonly localNodePath: string | null; readonly globalNodePath: string | null; readonly workspaceSymbolsExcludeLibrarySymbols: boolean; + readonly enableRegionDiagnostics: boolean; } export function areServiceConfigurationsEqual(a: TypeScriptServiceConfiguration, b: TypeScriptServiceConfiguration): boolean { @@ -162,6 +163,7 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu localNodePath: this.readLocalNodePath(configuration), globalNodePath: this.readGlobalNodePath(configuration), workspaceSymbolsExcludeLibrarySymbols: this.readWorkspaceSymbolsExcludeLibrarySymbols(configuration), + enableRegionDiagnostics: this.readEnableRegionDiagnostics(configuration), }; } @@ -267,4 +269,8 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu private readWebTypeAcquisition(configuration: vscode.WorkspaceConfiguration): boolean { return configuration.get('typescript.tsserver.web.typeAcquisition.enabled', false); } + + private readEnableRegionDiagnostics(configuration: vscode.WorkspaceConfiguration): boolean { + return configuration.get('typescript.tsserver.enableRegionDiagnostics', true); + } } diff --git a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts index 643c77ac357..83a7bb38639 100644 --- a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts +++ b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { DocumentSelector } from '../configuration/documentSelector'; import * as typeConverters from '../typeConverters'; import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; -import { conditionalRegistration, requireMinVersion, requireSomeCapability } from './util/dependentRegistration'; +import { conditionalRegistration, requireGlobalConfiguration, requireMinVersion, requireSomeCapability } from './util/dependentRegistration'; import protocol from '../tsServer/protocol/protocol'; import { API } from '../tsServer/api'; import { LanguageDescription } from '../configuration/languageDescription'; @@ -38,6 +38,8 @@ class CopyMetadata { } } +const settingId = 'experimental.updateImportsOnPaste'; + class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('text', 'jsts', 'pasteWithImports'); @@ -61,7 +63,7 @@ class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { token: vscode.CancellationToken, ): Promise { const config = vscode.workspace.getConfiguration(this._modeId, document.uri); - if (!config.get('experimental.updateImportsOnPaste')) { + if (!config.get(settingId, false)) { return; } @@ -93,13 +95,17 @@ class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { } } - const response = await this._client.execute('getPasteEdits', { + if (copiedFrom?.file === file) { + return; + } + + const response = await this._client.interruptGetErr(() => this._client.execute('getPasteEdits', { file, // TODO: only supports a single paste for now pastedText: [text], pasteLocations: ranges.map(typeConverters.Range.toTextSpan), copiedFrom - }, token); + }, token)); if (response.type !== 'response' || !response.body || token.isCancellationRequested) { return; } @@ -126,7 +132,8 @@ class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { export function register(selector: DocumentSelector, language: LanguageDescription, client: ITypeScriptServiceClient) { return conditionalRegistration([ requireSomeCapability(client, ClientCapability.Semantic), - requireMinVersion(client, API.v550), + requireMinVersion(client, API.v560), + requireGlobalConfiguration(language.id, settingId), ], () => { return vscode.languages.registerDocumentPasteEditProvider(selector.semantic, new DocumentPasteProvider(language.id, client), { providedPasteEditKinds: [DocumentPasteProvider.kind], diff --git a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts index 190e6a99bf7..990aefdfa56 100644 --- a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts @@ -34,6 +34,7 @@ export const enum DiagnosticKind { Syntax, Semantic, Suggestion, + RegionSemantic, } class FileDiagnostics { @@ -48,7 +49,8 @@ class FileDiagnostics { public updateDiagnostics( language: DiagnosticLanguage, kind: DiagnosticKind, - diagnostics: ReadonlyArray + diagnostics: ReadonlyArray, + ranges: ReadonlyArray | undefined ): boolean { if (language !== this.language) { this._diagnostics.clear(); @@ -61,6 +63,9 @@ class FileDiagnostics { return false; } + if (kind === DiagnosticKind.RegionSemantic) { + return this.updateRegionDiagnostics(diagnostics, ranges!); + } this._diagnostics.set(kind, diagnostics); return true; } @@ -83,6 +88,23 @@ class FileDiagnostics { } } + /** + * @param ranges The ranges whose diagnostics were updated. + */ + private updateRegionDiagnostics( + diagnostics: ReadonlyArray, + ranges: ReadonlyArray): boolean { + if (!this._diagnostics.get(DiagnosticKind.Semantic)) { + this._diagnostics.set(DiagnosticKind.Semantic, diagnostics); + return true; + } + const oldDiagnostics = this._diagnostics.get(DiagnosticKind.Semantic)!; + const newDiagnostics = oldDiagnostics.filter(diag => !ranges.some(range => diag.range.intersection(range))); + newDiagnostics.push(...diagnostics); + this._diagnostics.set(DiagnosticKind.Semantic, newDiagnostics); + return true; + } + private getSuggestionDiagnostics(settings: DiagnosticSettings) { const enableSuggestions = settings.getEnableSuggestions(this.language); return this.get(DiagnosticKind.Suggestion).filter(x => { @@ -284,15 +306,16 @@ export class DiagnosticsManager extends Disposable { file: vscode.Uri, language: DiagnosticLanguage, kind: DiagnosticKind, - diagnostics: ReadonlyArray + diagnostics: ReadonlyArray, + ranges: ReadonlyArray | undefined, ): void { let didUpdate = false; const entry = this._diagnostics.get(file); if (entry) { - didUpdate = entry.updateDiagnostics(language, kind, diagnostics); + didUpdate = entry.updateDiagnostics(language, kind, diagnostics, ranges); } else if (diagnostics.length) { const fileDiagnostics = new FileDiagnostics(file, language); - fileDiagnostics.updateDiagnostics(language, kind, diagnostics); + fileDiagnostics.updateDiagnostics(language, kind, diagnostics, ranges); this._diagnostics.set(file, fileDiagnostics); didUpdate = true; } diff --git a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts index f96f89452b9..bddd062b3e8 100644 --- a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts @@ -191,7 +191,6 @@ export default class FileConfigurationManager extends Disposable { includeCompletionsWithClassMemberSnippets: config.get('suggest.classMemberSnippets.enabled', true), includeCompletionsWithObjectLiteralMethodSnippets: config.get('suggest.objectLiteralMethodSnippets.enabled', true), autoImportFileExcludePatterns: this.getAutoImportFileExcludePatternsPreference(preferencesConfig, vscode.workspace.getWorkspaceFolder(document.uri)?.uri), - // @ts-expect-error until 5.3 #56090 preferTypeOnlyAutoImports: preferencesConfig.get('preferTypeOnlyAutoImports', false), useLabelDetailsInCompletionEntries: true, allowIncompleteCompletions: true, diff --git a/extensions/typescript-language-features/src/languageFeatures/refactor.ts b/extensions/typescript-language-features/src/languageFeatures/refactor.ts index 8dc06235753..0364b7fad3e 100644 --- a/extensions/typescript-language-features/src/languageFeatures/refactor.ts +++ b/extensions/typescript-language-features/src/languageFeatures/refactor.ts @@ -541,11 +541,12 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { + const response = await this.interruptGetErrIfNeeded(context, () => { const file = this.client.toOpenTsFilePath(document); if (!file) { return undefined; } + this.formattingOptionsManager.ensureConfigurationForDocument(document, token); const args: Proto.GetApplicableRefactorsRequestArgs = { @@ -595,6 +596,17 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider(context: vscode.CodeActionContext, f: () => R): R { + // Only interrupt diagnostics computation when code actions are explicitly + // (such as using the refactor command or a keybinding). This is a clear + // user action so we want to return results as quickly as possible. + if (context.triggerKind === vscode.CodeActionTriggerKind.Invoke) { + return this.client.interruptGetErr(f); + } else { + return f(); + } + } + public async resolveCodeAction( codeAction: TsCodeAction, token: vscode.CancellationToken, diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index a1927409869..7b95591604b 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -138,7 +138,11 @@ export default class LanguageProvider extends Disposable { this.client.bufferSyncSupport.requestAllDiagnostics(); } - public diagnosticsReceived(diagnosticsKind: DiagnosticKind, file: vscode.Uri, diagnostics: (vscode.Diagnostic & { reportUnnecessary: any; reportDeprecated: any })[]): void { + public diagnosticsReceived( + diagnosticsKind: DiagnosticKind, + file: vscode.Uri, + diagnostics: (vscode.Diagnostic & { reportUnnecessary: any; reportDeprecated: any })[], + ranges: vscode.Range[] | undefined): void { if (diagnosticsKind !== DiagnosticKind.Syntax && !this.client.hasCapabilityForResource(file, ClientCapability.Semantic)) { return; } @@ -175,7 +179,7 @@ export default class LanguageProvider extends Disposable { } } return true; - })); + }), ranges); } public configFileDiagnosticsReceived(file: vscode.Uri, diagnostics: vscode.Diagnostic[]): void { diff --git a/extensions/typescript-language-features/src/tsServer/api.ts b/extensions/typescript-language-features/src/tsServer/api.ts index 4f26db47513..4beb29d1b2b 100644 --- a/extensions/typescript-language-features/src/tsServer/api.ts +++ b/extensions/typescript-language-features/src/tsServer/api.ts @@ -38,6 +38,7 @@ export class API { public static readonly v544 = API.fromSimpleString('5.4.4'); public static readonly v540 = API.fromSimpleString('5.4.0'); public static readonly v550 = API.fromSimpleString('5.5.0'); + public static readonly v560 = API.fromSimpleString('5.6.0'); public static fromVersionString(versionString: string): API { let version = semver.valid(versionString); diff --git a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index 87c715982c2..32707f1c049 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -275,12 +275,12 @@ class SyncedBufferMap extends ResourceMap { } class PendingDiagnostics extends ResourceMap { - public getOrderedFileSet(): ResourceMap { + public getOrderedFileSet(): ResourceMap { const orderedResources = Array.from(this.entries()) .sort((a, b) => a.value - b.value) .map(entry => entry.resource); - const map = new ResourceMap(this._normalizePath, this.config); + const map = new ResourceMap(this._normalizePath, this.config); for (const resource of orderedResources) { map.set(resource, undefined); } @@ -292,7 +292,7 @@ class GetErrRequest { public static executeGetErrRequest( client: ITypeScriptServiceClient, - files: ResourceMap, + files: ResourceMap, onDone: () => void ) { return new GetErrRequest(client, files, onDone); @@ -303,7 +303,7 @@ class GetErrRequest { private constructor( private readonly client: ITypeScriptServiceClient, - public readonly files: ResourceMap, + public readonly files: ResourceMap, onDone: () => void ) { if (!this.isErrorReportingEnabled()) { @@ -313,19 +313,39 @@ class GetErrRequest { } const supportsSyntaxGetErr = this.client.apiVersion.gte(API.v440); - const allFiles = coalesce(Array.from(files.entries()) - .filter(entry => supportsSyntaxGetErr || client.hasCapabilityForResource(entry.resource, ClientCapability.Semantic)) + const fileEntries = Array.from(files.entries()).filter(entry => supportsSyntaxGetErr || client.hasCapabilityForResource(entry.resource, ClientCapability.Semantic)); + const allFiles = coalesce(fileEntries .map(entry => client.toTsFilePath(entry.resource))); if (!allFiles.length) { this._done = true; setImmediate(onDone); } else { - const request = this.areProjectDiagnosticsEnabled() + let request; + if (this.areProjectDiagnosticsEnabled()) { // Note that geterrForProject is almost certainly not the api we want here as it ends up computing far // too many diagnostics - ? client.executeAsync('geterrForProject', { delay: 0, file: allFiles[0] }, this._token.token) - : client.executeAsync('geterr', { delay: 0, files: allFiles }, this._token.token); + request = client.executeAsync('geterrForProject', { delay: 0, file: allFiles[0] }, this._token.token); + } + else { + let requestFiles; + if (this.areRegionDiagnosticsEnabled()) { + requestFiles = coalesce(fileEntries + .map(entry => { + const file = client.toTsFilePath(entry.resource); + const ranges = entry.value; + if (file && ranges) { + return typeConverters.Range.toFileRangesRequestArgs(file, ranges); + } + + return file; + })); + } + else { + requestFiles = allFiles; + } + request = client.executeAsync('geterr', { delay: 0, files: requestFiles }, this._token.token); + } request.finally(() => { if (this._done) { @@ -350,6 +370,10 @@ class GetErrRequest { return this.client.configuration.enableProjectDiagnostics && this.client.capabilities.has(ClientCapability.Semantic); } + private areRegionDiagnosticsEnabled() { + return this.client.configuration.enableRegionDiagnostics && this.client.apiVersion.gte(API.v560); + } + public cancel(): any { if (!this._done) { this._token.cancel(); @@ -722,7 +746,9 @@ export default class BufferSyncSupport extends Disposable { // Add all open TS buffers to the geterr request. They might be visible for (const buffer of this.syncedBuffers.values()) { - orderedFileSet.set(buffer.resource, undefined); + const editors = vscode.window.visibleTextEditors.filter(editor => editor.document.uri.toString() === buffer.resource.toString()); + const visibleRanges = editors.flatMap(editor => editor.visibleRanges); + orderedFileSet.set(buffer.resource, visibleRanges.length ? visibleRanges : undefined); } for (const { resource } of orderedFileSet.entries()) { diff --git a/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts b/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts index 4f02ed29427..f1b0cca26a4 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts +++ b/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts @@ -78,6 +78,7 @@ export enum EventName { syntaxDiag = 'syntaxDiag', semanticDiag = 'semanticDiag', suggestionDiag = 'suggestionDiag', + regionSemanticDiag = 'regionSemanticDiag', configFileDiag = 'configFileDiag', telemetry = 'telemetry', projectLanguageServiceState = 'projectLanguageServiceState', diff --git a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts index 900d66f37ab..747e7c22e37 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts +++ b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts @@ -19,70 +19,5 @@ declare module '../../../../node_modules/typescript/lib/typescript' { interface Response { readonly _serverType?: ServerType; } - - //#region MapCode - export interface MapCodeRequestArgs extends FileRequestArgs { - /** - * The files and changes to try and apply/map. - */ - mapping: MapCodeRequestDocumentMapping; - } - - export interface MapCodeRequestDocumentMapping { - /** - * The specific code to map/insert/replace in the file. - */ - contents: string[]; - - /** - * Areas of "focus" to inform the code mapper with. For example, cursor - * location, current selection, viewport, etc. Nested arrays denote - * priority: toplevel arrays are more important than inner arrays, and - * inner array priorities are based on items within that array. Items - * earlier in the arrays have higher priority. - */ - focusLocations?: TextSpan[][]; - } - - export interface MapCodeRequest extends FileRequest { - command: 'mapCode'; - arguments: MapCodeRequestArgs; - } - - export interface MapCodeResponse extends Response { - body: FileCodeEdits[] - } - //#endregion - - //#region Paste - export interface GetPasteEditsRequest extends Request { - command: 'getPasteEdits'; - arguments: GetPasteEditsRequestArgs; - } - - export interface GetPasteEditsRequestArgs extends FileRequestArgs { - /** The text that gets pasted in a file. */ - pastedText: string[]; - /** Locations of where the `pastedText` gets added in a file. If the length of the `pastedText` and `pastedLocations` are not the same, - * then the `pastedText` is combined into one and added at all the `pastedLocations`. - */ - pasteLocations: TextSpan[]; - /** The source location of each `pastedText`. If present, the length of `spans` must be equal to the length of `pastedText`. */ - copiedFrom?: { - file: string; - spans: TextSpan[]; - }; - } - - export interface GetPasteEditsResponse extends Response { - body: PasteEditsAction; - } - export interface PasteEditsAction { - edits: FileCodeEdits[]; - fixId?: {}; - } - //#endregion } } - - diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts index d03c7ea5df0..7dbde90f792 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts @@ -278,8 +278,7 @@ export class ElectronServiceProcessFactory implements TsServerProcessFactory { } const childProcess = execPath ? - child_process.spawn(JSON.stringify(execPath), [...execArgv, tsServerPath, ...runtimeArgs], { - shell: true, + child_process.spawn(execPath, [...execArgv, tsServerPath, ...runtimeArgs], { windowsHide: true, cwd: undefined, env, diff --git a/extensions/typescript-language-features/src/tsServer/spawner.ts b/extensions/typescript-language-features/src/tsServer/spawner.ts index 543140dbab5..364c0f07dae 100644 --- a/extensions/typescript-language-features/src/tsServer/spawner.ts +++ b/extensions/typescript-language-features/src/tsServer/spawner.ts @@ -234,7 +234,7 @@ export class TypeScriptServerSpawner { tsServerLog = { type: 'file', uri: logFilePath }; args.push('--logVerbosity', TsServerLogLevel.toString(configuration.tsServerLogLevel)); - args.push('--logFile', `"${logFilePath.fsPath}"`); + args.push('--logFile', logFilePath.fsPath); } } } diff --git a/extensions/typescript-language-features/src/typeConverters.ts b/extensions/typescript-language-features/src/typeConverters.ts index 58babe2bda3..067a1ff3c0a 100644 --- a/extensions/typescript-language-features/src/typeConverters.ts +++ b/extensions/typescript-language-features/src/typeConverters.ts @@ -26,14 +26,24 @@ export namespace Range { Math.max(0, start.line - 1), Math.max(start.offset - 1, 0), Math.max(0, end.line - 1), Math.max(0, end.offset - 1)); - export const toFileRangeRequestArgs = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({ - file, + // @ts-expect-error until ts 5.6 + export const toFileRange = (range: vscode.Range): Proto.FileRange => ({ startLine: range.start.line + 1, startOffset: range.start.character + 1, endLine: range.end.line + 1, endOffset: range.end.character + 1 }); + export const toFileRangeRequestArgs = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({ + file, + ...toFileRange(range) + }); + // @ts-expect-error until ts 5.6 + export const toFileRangesRequestArgs = (file: string, ranges: vscode.Range[]): Proto.FileRangesRequestArgs => ({ + file, + ranges: ranges.map(toFileRange) + }); + export const toFormattingRequestArgs = (file: string, range: vscode.Range): Proto.FormatRequestArgs => ({ file, line: range.start.line + 1, diff --git a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index da651e71044..c44bfc3ac3f 100644 --- a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -90,8 +90,8 @@ export default class TypeScriptServiceClientHost extends Disposable { services, allModeIds)); - this.client.onDiagnosticsReceived(({ kind, resource, diagnostics }) => { - this.diagnosticsReceived(kind, resource, diagnostics); + this.client.onDiagnosticsReceived(({ kind, resource, diagnostics, spans }) => { + this.diagnosticsReceived(kind, resource, diagnostics, spans); }, null, this._disposables); this.client.onConfigDiagnosticsReceived(diag => this.configFileDiagnosticsReceived(diag), null, this._disposables); @@ -236,14 +236,16 @@ export default class TypeScriptServiceClientHost extends Disposable { private async diagnosticsReceived( kind: DiagnosticKind, resource: vscode.Uri, - diagnostics: Proto.Diagnostic[] + diagnostics: Proto.Diagnostic[], + spans: Proto.TextSpan[] | undefined, ): Promise { const language = await this.findLanguage(resource); if (language) { language.diagnosticsReceived( kind, resource, - this.createMarkerDatas(diagnostics, language.diagnosticSource)); + this.createMarkerDatas(diagnostics, language.diagnosticSource), + spans?.map(span => typeConverters.Range.fromTextSpan(span))); } } diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 24742f99219..da6408b827b 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -37,6 +37,7 @@ export interface TsDiagnostics { readonly kind: DiagnosticKind; readonly resource: vscode.Uri; readonly diagnostics: Proto.Diagnostic[]; + readonly spans?: Proto.TextSpan[]; } interface ToCancelOnResourceChanged { @@ -947,7 +948,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType switch (event.event) { case EventName.syntaxDiag: case EventName.semanticDiag: - case EventName.suggestionDiag: { + case EventName.suggestionDiag: + case EventName.regionSemanticDiag: { // This event also roughly signals that projects have been loaded successfully (since the TS server is synchronous) this.loadingIndicator.reset(); @@ -956,7 +958,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType this._onDiagnosticsReceived.fire({ kind: getDiagnosticsKind(event), resource: this.toResource(diagnosticEvent.body.file), - diagnostics: diagnosticEvent.body.diagnostics + diagnostics: diagnosticEvent.body.diagnostics, + // @ts-expect-error until ts 5.6 + spans: diagnosticEvent.body.spans, }); } break; @@ -1261,6 +1265,7 @@ function getDiagnosticsKind(event: Proto.Event) { case 'syntaxDiag': return DiagnosticKind.Syntax; case 'semanticDiag': return DiagnosticKind.Semantic; case 'suggestionDiag': return DiagnosticKind.Suggestion; + case 'regionSemanticDiag': return DiagnosticKind.RegionSemantic; } throw new Error('Unknown dignostics kind'); } diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 868f7cfd4e7..7e8b96839f4 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -7,13 +7,14 @@ "enabledApiProposals": [ "activeComment", "authSession", - "defaultChatParticipant", "chatParticipantPrivate", + "chatProvider", "chatVariableResolver", - "contribViewsRemote", "contribStatusBarItems", + "contribViewsRemote", "createFileSystemWatcher", "customEditorMove", + "defaultChatParticipant", "diffCommand", "documentFiltersExclusive", "documentPaste", @@ -27,6 +28,8 @@ "findTextInFiles", "fsChunks", "interactive", + "languageStatusText", + "lmTools", "mappedEditsProvider", "notebookCellExecutionState", "notebookDeprecated", @@ -35,25 +38,24 @@ "notebookMime", "portsAttributes", "quickPickSortByLabel", - "languageStatusText", "resolvers", "scmActionButton", "scmSelectedProvider", "scmTextDocument", "scmValidation", "taskPresentationGroup", + "telemetry", "terminalDataWriteEvent", "terminalDimensions", "terminalShellIntegration", - "tunnels", "testObserver", "textSearchProvider", "timeline", "tokenInformation", "treeViewActiveItem", "treeViewReveal", - "workspaceTrust", - "telemetry" + "tunnels", + "workspaceTrust" ], "private": true, "activationEvents": [], @@ -63,6 +65,11 @@ }, "icon": "media/icon.png", "contributes": { + "languageModels": [ + { + "vendor": "test-lm-vendor" + } + ], "chatParticipants": [ { "id": "api-test.participant", @@ -242,7 +249,9 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "20.x" + "@types/node": "20.x", + "@types/node-forge": "^1.3.11", + "node-forge": "^1.3.1" }, "repository": { "type": "git", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts new file mode 100644 index 00000000000..178119a1197 --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { assertNoRpc, closeAllEditors, DeferredPromise, disposeAll } from '../utils'; + + +suite('lm', function () { + + let disposables: vscode.Disposable[] = []; + + setup(function () { + disposables = []; + }); + + teardown(async function () { + assertNoRpc(); + await closeAllEditors(); + disposeAll(disposables); + }); + + + test('lm request and stream', async function () { + + let p: vscode.Progress | undefined; + const defer = new DeferredPromise(); + + disposables.push(vscode.lm.registerChatModelProvider('test-lm', { + async provideLanguageModelResponse(_messages, _options, _extensionId, progress, _token) { + p = progress; + return defer.p; + }, + async provideTokenCount(_text, _token) { + return 1; + }, + }, { + name: 'test-lm', + version: '1.0.0', + family: 'test', + vendor: 'test-lm-vendor', + maxInputTokens: 100, + maxOutputTokens: 100, + })); + + const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); + assert.strictEqual(models.length, 1); + + const request = await models[0].sendRequest([vscode.LanguageModelChatMessage.User('Hello')]); + + // assert we have a request immediately + assert.ok(request); + assert.ok(p); + assert.strictEqual(defer.isSettled, false); + + let streamDone = false; + let responseText = ''; + + const pp = (async () => { + for await (const chunk of request.text) { + responseText += chunk; + } + streamDone = true; + })(); + + assert.strictEqual(responseText, ''); + assert.strictEqual(streamDone, false); + + p.report({ index: 0, part: 'Hello' }); + defer.complete(); + + await pp; + await new Promise(r => setTimeout(r, 1000)); + + assert.strictEqual(streamDone, true); + assert.strictEqual(responseText, 'Hello'); + }); + + test('lm request fail', async function () { + + disposables.push(vscode.lm.registerChatModelProvider('test-lm', { + async provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { + throw new Error('BAD'); + }, + async provideTokenCount(_text, _token) { + return 1; + }, + }, { + name: 'test-lm', + version: '1.0.0', + family: 'test', + vendor: 'test-lm-vendor', + maxInputTokens: 100, + maxOutputTokens: 100, + })); + + const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); + assert.strictEqual(models.length, 1); + + try { + await models[0].sendRequest([vscode.LanguageModelChatMessage.User('Hello')]); + assert.ok(false, 'EXPECTED error'); + } catch (error) { + assert.ok(error instanceof Error); + } + }); + + test('lm stream fail', async function () { + + const defer = new DeferredPromise(); + + disposables.push(vscode.lm.registerChatModelProvider('test-lm', { + async provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { + return defer.p; + }, + async provideTokenCount(_text, _token) { + return 1; + }, + }, { + name: 'test-lm', + version: '1.0.0', + family: 'test', + vendor: 'test-lm-vendor', + maxInputTokens: 100, + maxOutputTokens: 100, + })); + + const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); + assert.strictEqual(models.length, 1); + + const res = await models[0].sendRequest([vscode.LanguageModelChatMessage.User('Hello')]); + assert.ok(res); + + const result = (async () => { + for await (const _chunk of res.text) { + + } + })(); + + defer.error(new Error('STREAM FAIL')); + + try { + await result; + assert.ok(false, 'EXPECTED error'); + } catch (error) { + assert.ok(error); + // assert.ok(error instanceof Error); // todo@jrieken requires one more insiders + } + }); +}); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts new file mode 100644 index 00000000000..60f100c7c1f --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as https from 'https'; +import 'mocha'; +import { assertNoRpc } from '../utils'; +import { pki } from 'node-forge'; +import { AddressInfo } from 'net'; +import { resetCaches } from '@vscode/proxy-agent'; + +suite('vscode API - network proxy support', () => { + + teardown(async function () { + assertNoRpc(); + }); + + test('custom root certificate', async () => { + const keys = pki.rsa.generateKeyPair(2048); + const cert = pki.createCertificate(); + cert.publicKey = keys.publicKey; + cert.serialNumber = '01'; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(); + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); + const attrs = [{ + name: 'commonName', + value: 'localhost-proxy-test' + }]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.sign(keys.privateKey); + const certPEM = pki.certificateToPem(cert); + const privateKeyPEM = pki.privateKeyToPem(keys.privateKey); + + let resolvePort: (port: number) => void; + let rejectPort: (err: any) => void; + const port = new Promise((resolve, reject) => { + resolvePort = resolve; + rejectPort = reject; + }); + const server = https.createServer({ + key: privateKeyPEM, + cert: certPEM, + }, (_req, res) => { + res.end(); + }).listen(0, '127.0.0.1', () => { + const address = server.address(); + resolvePort((address as AddressInfo).port); + }).on('error', err => { + rejectPort(err); + }); + + // Using https.globalAgent because it is shared with proxyResolver.ts and mutable. + (https.globalAgent as any).testCertificates = [certPEM]; + resetCaches(); + + try { + const portNumber = await port; + await new Promise((resolve, reject) => { + https.get(`https://127.0.0.1:${portNumber}`, { servername: 'localhost-proxy-test' }, res => { + if (res.statusCode === 200) { + resolve(); + } else { + reject(new Error(`Unexpected status code: ${res.statusCode}`)); + } + }) + .on('error', reject); + }); + } finally { + delete (https.globalAgent as any).testCertificates; + resetCaches(); + server.close(); + } + }); +}); diff --git a/extensions/vscode-api-tests/yarn.lock b/extensions/vscode-api-tests/yarn.lock index 484fa0c5ac5..33a2f511927 100644 --- a/extensions/vscode-api-tests/yarn.lock +++ b/extensions/vscode-api-tests/yarn.lock @@ -7,6 +7,20 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== +"@types/node-forge@^1.3.11": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "20.14.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.6.tgz#f3c19ffc98c2220e18de259bb172dd4d892a6075" + integrity sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw== + dependencies: + undici-types "~5.26.4" + "@types/node@20.x": version "20.11.24" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" @@ -14,6 +28,11 @@ dependencies: undici-types "~5.26.4" +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" diff --git a/extensions/yarn.lock b/extensions/yarn.lock index fa4595ffa74..b981143bdd0 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -128,11 +128,11 @@ node-gyp-build "^4.3.0" braces@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" coffeescript@1.12.7: version "1.12.7" @@ -180,10 +180,10 @@ fast-plist@0.1.2: resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8" integrity sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg= -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -234,10 +234,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -typescript@5.4.5: - version "5.4.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" - integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== +typescript@^5.5.2: + version "5.5.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507" + integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew== vscode-grammar-updater@^1.1.0: version "1.1.0" diff --git a/package.json b/package.json index 49a49395cff..d2e4666bef5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.90.0", - "distro": "4729d9dae7d87c18fbe0614f875a60672e2b8603", + "version": "1.91.0", + "distro": "a08799837ca498c02f445ca7a896f446419af238", "author": { "name": "Microsoft Corporation" }, @@ -77,7 +77,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.4", - "@vscode/proxy-agent": "^0.19.0", + "@vscode/proxy-agent": "^0.21.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.6-vscode", @@ -86,18 +86,18 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-image": "0.9.0-beta.17", - "@xterm/addon-search": "0.16.0-beta.17", - "@xterm/addon-serialize": "0.14.0-beta.17", - "@xterm/addon-unicode11": "0.9.0-beta.17", - "@xterm/addon-webgl": "0.19.0-beta.17", - "@xterm/headless": "5.6.0-beta.17", - "@xterm/xterm": "5.6.0-beta.17", - "graceful-fs": "4.2.11", + "@xterm/addon-clipboard": "0.2.0-beta.4", + "@xterm/addon-image": "0.9.0-beta.21", + "@xterm/addon-search": "0.16.0-beta.21", + "@xterm/addon-serialize": "0.14.0-beta.21", + "@xterm/addon-unicode11": "0.9.0-beta.21", + "@xterm/addon-webgl": "0.19.0-beta.21", + "@xterm/headless": "5.6.0-beta.21", + "@xterm/xterm": "5.6.0-beta.21", "he": "^1.2.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", - "jschardet": "3.0.0", + "jschardet": "3.1.2", "kerberos": "^2.0.1", "minimist": "^1.2.6", "native-is-elevated": "0.7.0", @@ -120,7 +120,6 @@ "@swc/core": "1.3.62", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", - "@types/graceful-fs": "4.1.2", "@types/gulp-svgmin": "^1.2.1", "@types/he": "^1.2.0", "@types/http-proxy-agent": "^2.0.1", @@ -228,7 +227,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.5.0-dev.20240521", + "typescript": "^5.6.0-dev.20240618", "util": "^0.12.4", "vscode-nls-dev": "^3.3.1", "webpack": "^5.91.0", diff --git a/product.json b/product.json index 88559d514ff..1b422c44e72 100644 --- a/product.json +++ b/product.json @@ -54,8 +54,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.90.0", - "sha256": "1317dd7d1ac50641c1534a3e957ecbc94349f4fbd897acb916da11eea3208a66", + "version": "1.91.0", + "sha256": "53b99146c7fa280f00c74414e09721530c622bf3e5eac2c967ddfb9906b51c80", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", @@ -234,6 +234,9 @@ "https://open-vsx.org", "https://github.com/posit-dev/positron" ], + "extensionsEnabledWithApiProposalVersion": [ + "GitHub.copilot-chat" + ], "extensionEnabledApiProposals": { "ms-vscode.vscode-selfhost-test-provider": [ "testObserver", @@ -266,7 +269,6 @@ "resolvers" ], "ms-python.python": [ - "positronResolveSymlinks", "contribEditorContentMenu", "quickPickSortByLabel", "portsAttributes", @@ -369,6 +371,7 @@ "codeActionRanges", "commentingRangeHint", "commentReactor", + "commentReveal", "commentThreadApplicability", "contribAccessibilityHelpContent", "contribCommentEditorActionsMenu", @@ -408,6 +411,7 @@ "codeActionAI", "findTextInFiles", "textSearchProvider", + "commentReveal", "contribSourceControlInputBoxMenu", "contribCommentEditorActionsMenu", "contribCommentThreadAdditionalMenu", @@ -419,7 +423,8 @@ "testObserver", "aiTextSearchProvider", "documentFiltersExclusive", - "chatParticipantPrivate" + "chatParticipantPrivate", + "lmTools" ], "GitHub.remotehub": [ "contribRemoteHelp", diff --git a/remote/package.json b/remote/package.json index b84f5136fb1..5b0e80182ea 100644 --- a/remote/package.json +++ b/remote/package.json @@ -8,24 +8,24 @@ "@parcel/watcher": "2.1.0", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.19.0", + "@vscode/proxy-agent": "^0.21.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-image": "0.9.0-beta.17", - "@xterm/addon-search": "0.16.0-beta.17", - "@xterm/addon-serialize": "0.14.0-beta.17", - "@xterm/addon-unicode11": "0.9.0-beta.17", - "@xterm/addon-webgl": "0.19.0-beta.17", - "@xterm/headless": "5.6.0-beta.17", - "@xterm/xterm": "5.6.0-beta.17", + "@xterm/addon-clipboard": "0.2.0-beta.4", + "@xterm/addon-image": "0.9.0-beta.21", + "@xterm/addon-search": "0.16.0-beta.21", + "@xterm/addon-serialize": "0.14.0-beta.21", + "@xterm/addon-unicode11": "0.9.0-beta.21", + "@xterm/addon-webgl": "0.19.0-beta.21", + "@xterm/headless": "5.6.0-beta.21", + "@xterm/xterm": "5.6.0-beta.21", "cookie": "^0.4.0", - "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", - "jschardet": "3.0.0", + "jschardet": "3.1.2", "kerberos": "^2.0.1", "minimist": "^1.2.6", "native-watchdog": "^1.4.1", diff --git a/remote/web/package.json b/remote/web/package.json index d1370563070..0d240e305cc 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -7,14 +7,15 @@ "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-image": "0.9.0-beta.17", - "@xterm/addon-search": "0.16.0-beta.17", - "@xterm/addon-serialize": "0.14.0-beta.17", - "@xterm/addon-unicode11": "0.9.0-beta.17", - "@xterm/addon-webgl": "0.19.0-beta.17", - "@xterm/xterm": "5.6.0-beta.17", + "@xterm/addon-clipboard": "0.2.0-beta.4", + "@xterm/addon-image": "0.9.0-beta.21", + "@xterm/addon-search": "0.16.0-beta.21", + "@xterm/addon-serialize": "0.14.0-beta.21", + "@xterm/addon-unicode11": "0.9.0-beta.21", + "@xterm/addon-webgl": "0.19.0-beta.21", + "@xterm/xterm": "5.6.0-beta.21", "he": "^1.2.0", - "jschardet": "3.0.0", + "jschardet": "3.1.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-window": "^1.8.8", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 32c8747deaf..6b4d55c2245 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -55,50 +55,62 @@ resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" integrity sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g== -"@xterm/addon-image@0.9.0-beta.17": - version "0.9.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.17.tgz#343d0665a6060d4f893b4f2d32de6ccbbd00bb63" - integrity sha512-g0r2hpBcLABY5as4llsMP36RHtkWooEn7tf+7U0/hTndJoCAvs4uGDqZNQigFgeAM3lJ4PnRYh4lfnEh9bGt8A== - -"@xterm/addon-search@0.16.0-beta.17": - version "0.16.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.17.tgz#7cb01c7f498405909d37040884ee22d1889a36d2" - integrity sha512-wBfxmWOeqG6HHHE5mVamDJ75zBdHC35ERNy5/aTpQsQsyxrnV0Ks76c8ZVTaTu9wyBCAyx7UmZT42Ot80khY/g== - -"@xterm/addon-serialize@0.14.0-beta.17": - version "0.14.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.17.tgz#1cb8e35c0d118060a807adb340624fa7f80dd9c5" - integrity sha512-/c3W39kdRgGGYDoYjXb5HrUC421qwPn6NryAT4WJuJWnyMtFbe2DPwKsTfHuCBPiPyovS3a9j950Md3O3YXDZA== - -"@xterm/addon-unicode11@0.9.0-beta.17": - version "0.9.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.17.tgz#b5558148029a796c6a6d78e2a8b7255f92a51530" - integrity sha512-z7v8uojFVrO1aLSWtnz5MzSrfWRT8phde7kh9ufqHLBv7YYtMHxlPVjSuW8PZ2h4eY1LOZf6icUAzrmyJmJ7Kg== - -"@xterm/addon-webgl@0.19.0-beta.17": - version "0.19.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.17.tgz#68ad9e68dd1cf581b391971de33f5c04966b0d8e" - integrity sha512-X8ObRgoZl7UZTgdndM+mpSO3hLzAhWKoXXrGvUQg/7XabRKAPrQ2XvdyZm04nYwibE6Tpit2h5kkxjlVqupIig== - -"@xterm/xterm@5.6.0-beta.17": - version "5.6.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.17.tgz#67ce2e2ff45bd6cc9f26d455d5522c6c4a122ed9" - integrity sha512-+wAv8PhaGQSN9yXWIa8EFtT33pbrA4lZakMB1P05fr+DQ7zoH66QOAUoDY95uOf/4+S6Ihz8wzP2+FH8zETQEA== +"@xterm/addon-clipboard@0.2.0-beta.4": + version "0.2.0-beta.4" + resolved "https://registry.yarnpkg.com/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.4.tgz#9911baaebfbc07a698ae62366a596bfdeac8fa7e" + integrity sha512-p2KGTFUDK4YFthCgfsv2wT66JDTZPcIuoWeDT+TmSFbS1smDPTMCyM/rDDkGY+duHRcQsIMVzGC+2NRb/exX6A== + dependencies: + js-base64 "^3.7.5" + +"@xterm/addon-image@0.9.0-beta.21": + version "0.9.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.21.tgz#64fe50ee623f3e518574e1cbbe649cc0c0d60265" + integrity sha512-kTArrrS7K5+WYTTO8Ktt1aYxKTO4/jUm3KmyvPVjf9iw7OhLtG9mU+X9dXo56DTAqmbIUfJgY3OQbWffcyNk7w== + +"@xterm/addon-search@0.16.0-beta.21": + version "0.16.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.21.tgz#b8a20e83c1ff24afa675c3723244b2068255688d" + integrity sha512-RVn8yRx+w6R7abWiIttyAR0+Myh+XCYOLAkwco3iIYgzlztmox3Qp6YNzWJj0G8iwSvzxaSu7Fbjbb2PXTOSIg== + +"@xterm/addon-serialize@0.14.0-beta.21": + version "0.14.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.21.tgz#a074c5fdd2105c07574e6848babefef2905d84cb" + integrity sha512-Eg1QT2WG0pAIV+RrPv921+dVQvQqMhoFv2DQfMYDcqNbD2mTvIbX/ecEMb1bmn3WI0jNNomQ8UHZRFNRbDA+BA== + +"@xterm/addon-unicode11@0.9.0-beta.21": + version "0.9.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.21.tgz#dc843df701e518bc459e77dcd4fd65fe49adbb4b" + integrity sha512-IiHYZ+88m5MCoAyOHWQ4xXzecOh6FsDDr8lZpJktbFHyzYjBlIDQ6z9cJg+3ApApfo5Xosnmzjs27kf7wG2L0w== + +"@xterm/addon-webgl@0.19.0-beta.21": + version "0.19.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.21.tgz#68b92a47bf6768babd57bfbaf3ac97a7c670d8df" + integrity sha512-YV8Aaxp4QokXXehSCJ7NvudZKPDyBiXv4HqENqDpQllCj4hOWC5xJYSoFoPtu5+UhlzfqqvYRX/Il7QegPFPDg== + +"@xterm/xterm@5.6.0-beta.21": + version "5.6.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.21.tgz#87a4e45752e5708cffc5c583d7f15e107313eb4e" + integrity sha512-1tLJaGudNSg1hEC+ZwUU7PiUvzURzKB5v1IRaJdmZK81ZCxvEF6Qfo281pTZsZFnv2iOWqFEC0C5uRmBXLm0lQ== he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +js-base64@^3.7.5: + version "3.7.7" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" + integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== + "js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -jschardet@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" - integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== +jschardet@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.1.2.tgz#9bf4364deba0677fe9e3bd9e29eda57febf2e9db" + integrity sha512-mw3CBZGzW8nUBPYhFU2ztZ/kJ6NClQUQVpyzvFMfznZsoC///ZQ30J2RCUanNsr5yF22LqhgYr/lj807/ZleWA== loose-envify@^1.1.0: version "1.4.0" diff --git a/remote/yarn.lock b/remote/yarn.lock index 4241bf03b14..c8e84fc81e1 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -66,10 +66,10 @@ resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48" integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg== -"@vscode/proxy-agent@^0.19.0": - version "0.19.1" - resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.19.1.tgz#d9640d85df1c48885580b68bb4b2b54e17f5332c" - integrity sha512-cs1VOx6d5n69HhgzK0cWeyfudJt+9LdJi/vtgRRxxwisWKg4h83B3+EUJ4udF5SEkJgMBp3oU0jheZVt43ImnQ== +"@vscode/proxy-agent@^0.21.0": + version "0.21.0" + resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.21.0.tgz#93c818b863ad20b42679032ecc1e3ecdc6306f12" + integrity sha512-9YcpBq+ZhMr3EQY/5ScyHc9kIIU/AcYOQn3DXq0N9tl81ViVsUvii3Fh+FAtD0YQ/qWtDfGxt8VCWZtuyh2D0g== dependencies: "@tootallnate/once" "^3.0.0" agent-base "^7.0.1" @@ -122,40 +122,47 @@ resolved "https://registry.yarnpkg.com/@vscode/windows-registry/-/windows-registry-1.1.0.tgz#03dace7c29c46f658588b9885b9580e453ad21f9" integrity sha512-5AZzuWJpGscyiMOed0IuyEwt6iKmV5Us7zuwCDCFYMIq7tsvooO9BUiciywsvuthGz6UG4LSpeDeCxvgMVhnIw== -"@xterm/addon-image@0.9.0-beta.17": - version "0.9.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.17.tgz#343d0665a6060d4f893b4f2d32de6ccbbd00bb63" - integrity sha512-g0r2hpBcLABY5as4llsMP36RHtkWooEn7tf+7U0/hTndJoCAvs4uGDqZNQigFgeAM3lJ4PnRYh4lfnEh9bGt8A== - -"@xterm/addon-search@0.16.0-beta.17": - version "0.16.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.17.tgz#7cb01c7f498405909d37040884ee22d1889a36d2" - integrity sha512-wBfxmWOeqG6HHHE5mVamDJ75zBdHC35ERNy5/aTpQsQsyxrnV0Ks76c8ZVTaTu9wyBCAyx7UmZT42Ot80khY/g== - -"@xterm/addon-serialize@0.14.0-beta.17": - version "0.14.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.17.tgz#1cb8e35c0d118060a807adb340624fa7f80dd9c5" - integrity sha512-/c3W39kdRgGGYDoYjXb5HrUC421qwPn6NryAT4WJuJWnyMtFbe2DPwKsTfHuCBPiPyovS3a9j950Md3O3YXDZA== - -"@xterm/addon-unicode11@0.9.0-beta.17": - version "0.9.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.17.tgz#b5558148029a796c6a6d78e2a8b7255f92a51530" - integrity sha512-z7v8uojFVrO1aLSWtnz5MzSrfWRT8phde7kh9ufqHLBv7YYtMHxlPVjSuW8PZ2h4eY1LOZf6icUAzrmyJmJ7Kg== - -"@xterm/addon-webgl@0.19.0-beta.17": - version "0.19.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.17.tgz#68ad9e68dd1cf581b391971de33f5c04966b0d8e" - integrity sha512-X8ObRgoZl7UZTgdndM+mpSO3hLzAhWKoXXrGvUQg/7XabRKAPrQ2XvdyZm04nYwibE6Tpit2h5kkxjlVqupIig== - -"@xterm/headless@5.6.0-beta.17": - version "5.6.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.6.0-beta.17.tgz#bff1d67c9c061c57adff22571e733d54e3aba2b7" - integrity sha512-ehS7y/XRqX1ppx4RPiYc0vu0SdIQ91aA4lSN/2XNOf3IGdP0A38Q7a0T6mzqxRGZKiiyA0kTR1szr78wnY+wmA== - -"@xterm/xterm@5.6.0-beta.17": - version "5.6.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.17.tgz#67ce2e2ff45bd6cc9f26d455d5522c6c4a122ed9" - integrity sha512-+wAv8PhaGQSN9yXWIa8EFtT33pbrA4lZakMB1P05fr+DQ7zoH66QOAUoDY95uOf/4+S6Ihz8wzP2+FH8zETQEA== +"@xterm/addon-clipboard@0.2.0-beta.4": + version "0.2.0-beta.4" + resolved "https://registry.yarnpkg.com/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.4.tgz#9911baaebfbc07a698ae62366a596bfdeac8fa7e" + integrity sha512-p2KGTFUDK4YFthCgfsv2wT66JDTZPcIuoWeDT+TmSFbS1smDPTMCyM/rDDkGY+duHRcQsIMVzGC+2NRb/exX6A== + dependencies: + js-base64 "^3.7.5" + +"@xterm/addon-image@0.9.0-beta.21": + version "0.9.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.21.tgz#64fe50ee623f3e518574e1cbbe649cc0c0d60265" + integrity sha512-kTArrrS7K5+WYTTO8Ktt1aYxKTO4/jUm3KmyvPVjf9iw7OhLtG9mU+X9dXo56DTAqmbIUfJgY3OQbWffcyNk7w== + +"@xterm/addon-search@0.16.0-beta.21": + version "0.16.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.21.tgz#b8a20e83c1ff24afa675c3723244b2068255688d" + integrity sha512-RVn8yRx+w6R7abWiIttyAR0+Myh+XCYOLAkwco3iIYgzlztmox3Qp6YNzWJj0G8iwSvzxaSu7Fbjbb2PXTOSIg== + +"@xterm/addon-serialize@0.14.0-beta.21": + version "0.14.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.21.tgz#a074c5fdd2105c07574e6848babefef2905d84cb" + integrity sha512-Eg1QT2WG0pAIV+RrPv921+dVQvQqMhoFv2DQfMYDcqNbD2mTvIbX/ecEMb1bmn3WI0jNNomQ8UHZRFNRbDA+BA== + +"@xterm/addon-unicode11@0.9.0-beta.21": + version "0.9.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.21.tgz#dc843df701e518bc459e77dcd4fd65fe49adbb4b" + integrity sha512-IiHYZ+88m5MCoAyOHWQ4xXzecOh6FsDDr8lZpJktbFHyzYjBlIDQ6z9cJg+3ApApfo5Xosnmzjs27kf7wG2L0w== + +"@xterm/addon-webgl@0.19.0-beta.21": + version "0.19.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.21.tgz#68b92a47bf6768babd57bfbaf3ac97a7c670d8df" + integrity sha512-YV8Aaxp4QokXXehSCJ7NvudZKPDyBiXv4HqENqDpQllCj4hOWC5xJYSoFoPtu5+UhlzfqqvYRX/Il7QegPFPDg== + +"@xterm/headless@5.6.0-beta.21": + version "5.6.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.6.0-beta.21.tgz#110fa33b59f4bf2d1de188e318bb944c8d774e97" + integrity sha512-RtKsv7KZb/ee8hwkvMNYuUofDoBR/KWUjoB5mo10C+dHyDJcMYiG2k48cAvcaJRjPH721iOELORKQk3NAlowkg== + +"@xterm/xterm@5.6.0-beta.21": + version "5.6.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.21.tgz#87a4e45752e5708cffc5c583d7f15e107313eb4e" + integrity sha512-1tLJaGudNSg1hEC+ZwUU7PiUvzURzKB5v1IRaJdmZK81ZCxvEF6Qfo281pTZsZFnv2iOWqFEC0C5uRmBXLm0lQ== agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: version "7.1.0" @@ -186,11 +193,11 @@ bl@^4.0.3: readable-stream "^3.4.0" braces@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" buffer-crc32@~0.2.3: version "0.2.13" @@ -263,10 +270,10 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -289,7 +296,7 @@ github-from-package@0.0.0: resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= -graceful-fs@4.2.11, graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -347,10 +354,15 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -jschardet@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" - integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== +js-base64@^3.7.5: + version "3.7.7" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" + integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== + +jschardet@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.1.2.tgz#9bf4364deba0677fe9e3bd9e29eda57febf2e9db" + integrity sha512-mw3CBZGzW8nUBPYhFU2ztZ/kJ6NClQUQVpyzvFMfznZsoC///ZQ30J2RCUanNsr5yF22LqhgYr/lj807/ZleWA== jsonfile@^6.0.1: version "6.1.0" diff --git a/resources/linux/snap/snapcraft.yaml b/resources/linux/snap/snapcraft.yaml index b7b93f4c59c..1d7412bdc71 100644 --- a/resources/linux/snap/snapcraft.yaml +++ b/resources/linux/snap/snapcraft.yaml @@ -30,15 +30,18 @@ parts: - libcurl3-gnutls - libcurl3-nss - libcurl4 + - libegl1 - libdrm2 - libgbm1 - libgl1 + - libgles2 - libglib2.0-0 - libgtk-3-0 - libibus-1.0-5 - libnss3 - libpango-1.0-0 - libsecret-1-0 + - libwayland-egl1 - libxcomposite1 - libxdamage1 - libxfixes3 diff --git a/scripts/code.bat b/scripts/code.bat index 008c54fcbde..7f48b753559 100644 --- a/scripts/code.bat +++ b/scripts/code.bat @@ -23,9 +23,16 @@ set VSCODE_CLI=1 set ELECTRON_ENABLE_LOGGING=1 set ELECTRON_ENABLE_STACK_DUMPING=1 +set DISABLE_TEST_EXTENSION="--disable-extension=vscode.vscode-api-tests" +for %%A in (%*) do ( + if "%%~A"=="--extensionTestsPath" ( + set DISABLE_TEST_EXTENSION="" + ) +) + :: Launch Code -%CODE% . %* +%CODE% . %DISABLE_TEST_EXTENSION% %* goto end :builtin diff --git a/scripts/code.sh b/scripts/code.sh index 24929fdf351..c29b632cbcb 100755 --- a/scripts/code.sh +++ b/scripts/code.sh @@ -42,8 +42,13 @@ function code() { export ELECTRON_ENABLE_STACK_DUMPING=1 export ELECTRON_ENABLE_LOGGING=1 + DISABLE_TEST_EXTENSION="--disable-extension=vscode.vscode-api-tests" + if [[ "$@" == *"--extensionTestsPath"* ]]; then + DISABLE_TEST_EXTENSION="" + fi + # Launch Code - exec "$CODE" . "$@" + exec "$CODE" . $DISABLE_TEST_EXTENSION "$@" } function code-wsl() diff --git a/scripts/xterm-update.js b/scripts/xterm-update.js index 851b296af62..8ede619160b 100644 --- a/scripts/xterm-update.js +++ b/scripts/xterm-update.js @@ -8,6 +8,7 @@ const path = require('path'); const moduleNames = [ '@xterm/xterm', + '@xterm/addon-clipboard', '@xterm/addon-image', '@xterm/addon-search', '@xterm/addon-serialize', diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index c011b3f0f76..4757ff291e2 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -8,6 +8,10 @@ //@ts-check 'use strict'; +/** + * @import { ISandboxConfiguration } from './vs/base/parts/sandbox/common/sandboxTypes' + */ + /* eslint-disable no-restricted-globals */ // Simple module style to support node.js and browser environments @@ -29,8 +33,6 @@ const safeProcess = preloadGlobals.process; /** - * @typedef {import('./vs/base/parts/sandbox/common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration - * * @param {string[]} modulePaths * @param {(result: unknown, configuration: ISandboxConfiguration) => Promise | undefined} resultCallback * @param {{ @@ -124,6 +126,7 @@ 'vscode-oniguruma': `${baseNodeModulesPath}/vscode-oniguruma/release/main.js`, 'vsda': `${baseNodeModulesPath}/vsda/index.js`, '@xterm/xterm': `${baseNodeModulesPath}/@xterm/xterm/lib/xterm.js`, + '@xterm/addon-clipboard': `${baseNodeModulesPath}/@xterm/addon-clipboard/lib/addon-clipboard.js`, '@xterm/addon-image': `${baseNodeModulesPath}/@xterm/addon-image/lib/addon-image.js`, '@xterm/addon-search': `${baseNodeModulesPath}/@xterm/addon-search/lib/addon-search.js`, '@xterm/addon-serialize': `${baseNodeModulesPath}/@xterm/addon-serialize/lib/addon-serialize.js`, diff --git a/src/main.js b/src/main.js index 9fe5654081d..1b3d4632fad 100644 --- a/src/main.js +++ b/src/main.js @@ -7,9 +7,9 @@ 'use strict'; /** - * @typedef {import('./vs/base/common/product').IProductConfiguration} IProductConfiguration - * @typedef {import('./vs/base/node/languagePacks').NLSConfiguration} NLSConfiguration - * @typedef {import('./vs/platform/environment/common/argv').NativeParsedArgs} NativeParsedArgs + * @import { IProductConfiguration } from './vs/base/common/product' + * @import { NLSConfiguration } from './vs/base/node/languagePacks' + * @import { NativeParsedArgs } from './vs/platform/environment/common/argv' */ const perf = require('./vs/base/common/performance'); @@ -21,7 +21,7 @@ const os = require('os'); const bootstrap = require('./bootstrap'); const bootstrapNode = require('./bootstrap-node'); const { getUserDataPath } = require('./vs/platform/environment/node/userDataPath'); -const { stripComments } = require('./vs/base/common/stripComments'); +const { parse } = require('./vs/base/common/jsonc'); const { getUNCHost, addUNCHostToAllowlist } = require('./vs/base/node/unc'); /** @type {Partial} */ // @ts-ignore @@ -205,7 +205,10 @@ function configureCommandlineSwitchesSync(cliArgs) { 'force-color-profile', // disable LCD font rendering, a Chromium flag - 'disable-lcd-text' + 'disable-lcd-text', + + // bypass any specified proxy for the given semi-colon-separated list of hosts + 'proxy-bypass-list' ]; if (process.platform === 'linux') { @@ -243,10 +246,7 @@ function configureCommandlineSwitchesSync(cliArgs) { app.commandLine.appendSwitch(argvKey); } } else if (argvValue) { - if (argvKey === 'force-color-profile') { - // Color profile - app.commandLine.appendSwitch(argvKey, argvValue); - } else if (argvKey === 'password-store') { + if (argvKey === 'password-store') { // Password store // TODO@TylerLeonhardt: Remove this migration in 3 months let migratedArgvValue = argvValue; @@ -254,6 +254,8 @@ function configureCommandlineSwitchesSync(cliArgs) { migratedArgvValue = 'gnome-libsecret'; } app.commandLine.appendSwitch(argvKey, migratedArgvValue); + } else { + app.commandLine.appendSwitch(argvKey, argvValue); } } } @@ -294,6 +296,13 @@ function configureCommandlineSwitchesSync(cliArgs) { `CalculateNativeWinOcclusion,${app.commandLine.getSwitchValue('disable-features')}`; app.commandLine.appendSwitch('disable-features', featuresToDisable); + // Blink features to configure. + // `FontMatchingCTMigration` - Siwtch font matching on macOS to CoreText (Refs https://github.com/microsoft/vscode/issues/214390). + // TODO(deepak1556): Enable this feature again after updating to Electron 30. + const blinkFeaturesToDisable = + `FontMatchingCTMigration,${app.commandLine.getSwitchValue('disable-blink-features')}`; + app.commandLine.appendSwitch('disable-blink-features', blinkFeaturesToDisable); + // Support JS Flags const jsFlags = getJSFlags(cliArgs); if (jsFlags) { @@ -309,7 +318,7 @@ function readArgvConfigSync() { const argvConfigPath = getArgvConfigPath(); let argvConfig; try { - argvConfig = JSON.parse(stripComments(fs.readFileSync(argvConfigPath).toString())); + argvConfig = parse(fs.readFileSync(argvConfigPath).toString()); } catch (error) { if (error && error.code === 'ENOENT') { createDefaultArgvConfigSync(argvConfigPath); diff --git a/src/server-main.js b/src/server-main.js index 81e88e118f7..e5feac4a627 100644 --- a/src/server-main.js +++ b/src/server-main.js @@ -5,6 +5,10 @@ // @ts-check +/** + * @import { IServerAPI } from './vs/server/node/remoteExtensionHostAgentServer' + */ + const perf = require('./vs/base/common/performance'); const performance = require('perf_hooks').performance; const product = require('../product.json'); @@ -45,9 +49,6 @@ async function start() { return; } - /** - * @typedef { import('./vs/server/node/remoteExtensionHostAgentServer').IServerAPI } IServerAPI - */ /** @type {IServerAPI | null} */ let _remoteExtensionHostAgentServer = null; /** @type {Promise | null} */ diff --git a/src/tsconfig.json b/src/tsconfig.json index 4ba199d358a..55c0f69626a 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { + "esModuleInterop": true, "jsx": "react", "removeComments": false, "preserveConstEnums": true, @@ -34,6 +35,16 @@ "./main.js", "./server-main.js", "./server-cli.js", + "./vs/base/common/jsonc.js", + "./vs/base/common/performance.js", + "./vs/base/node/unc.js", + "./vs/base/node/languagePacks.js", + "./vs/platform/environment/node/userDataPath.js", + "./vs/base/parts/sandbox/electron-sandbox/preload-aux.js", + "./vs/base/parts/sandbox/electron-sandbox/preload.js", + "./vs/code/electron-sandbox/processExplorer/processExplorer.js", + "./vs/code/electron-sandbox/workbench/workbench.js", + "./vs/workbench/contrib/issue/electron-sandbox/issueReporter.js", "./typings", "./vs/**/*.ts", // --- Start Positron --- diff --git a/src/tsconfig.tsec.json b/src/tsconfig.tsec.json index d2524df22d4..d822b0a4e89 100644 --- a/src/tsconfig.tsec.json +++ b/src/tsconfig.tsec.json @@ -10,6 +10,7 @@ ] }, "exclude": [ + "./vs/workbench/contrib/webview/browser/pre/service-worker.js", "*/test/*", "**/*.test.ts" ] diff --git a/src/vs/base/browser/dnd.ts b/src/vs/base/browser/dnd.ts index e55b238b08d..96259a4e63d 100644 --- a/src/vs/base/browser/dnd.ts +++ b/src/vs/base/browser/dnd.ts @@ -100,7 +100,7 @@ export function applyDragImage(event: DragEvent, label: string | null, clazz: st event.dataTransfer.setDragImage(dragImage, -10, -10); // Removes the element when the DND operation is done - setTimeout(() => ownerDocument.body.removeChild(dragImage), 0); + setTimeout(() => dragImage.remove(), 0); } } diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 3614986c445..66d30c3aca3 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -18,6 +18,7 @@ import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { hash } from 'vs/base/common/hash'; import { CodeWindow, ensureCodeWindow, mainWindow } from 'vs/base/browser/window'; +import { isPointWithinTriangle } from 'vs/base/common/numbers'; export interface IRegisteredCodeWindow { readonly window: CodeWindow; @@ -967,7 +968,7 @@ export function createStyleSheet(container: HTMLElement = mainWindow.document.he container.appendChild(style); if (disposableStore) { - disposableStore.add(toDisposable(() => container.removeChild(style))); + disposableStore.add(toDisposable(() => style.remove())); } // With as container, the stylesheet becomes global and is tracked @@ -1004,7 +1005,7 @@ function cloneGlobalStyleSheet(globalStylesheet: HTMLStyleElement, globalStylesh const clone = globalStylesheet.cloneNode(true) as HTMLStyleElement; targetWindow.document.head.appendChild(clone); - disposables.add(toDisposable(() => targetWindow.document.head.removeChild(clone))); + disposables.add(toDisposable(() => clone.remove())); for (const rule of getDynamicStyleSheetRules(globalStylesheet)) { clone.sheet?.insertRule(rule.cssText, clone.sheet?.cssRules.length); @@ -1726,7 +1727,7 @@ export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void anchor.click(); // Ensure to remove the element from DOM eventually - setTimeout(() => activeWindow.document.body.removeChild(anchor)); + setTimeout(() => anchor.remove()); } export function triggerUpload(): Promise { @@ -1749,7 +1750,7 @@ export function triggerUpload(): Promise { input.click(); // Ensure to remove the element from DOM eventually - setTimeout(() => activeWindow.document.body.removeChild(input)); + setTimeout(() => input.remove()); }); } @@ -2372,6 +2373,107 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia return result; } +export function svgElem + (tag: TTag): + TagToRecord extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function svgElem + (tag: TTag, children: [...T]): + (ArrayToObj & TagToRecord) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function svgElem + (tag: TTag, attributes: Partial>>): + TagToRecord extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function svgElem + (tag: TTag, attributes: Partial>>, children: [...T]): + (ArrayToObj & TagToRecord) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function svgElem(tag: string, ...args: [] | [attributes: { $: string } & Partial> | Record, children?: any[]] | [children: any[]]): Record { + let attributes: { $?: string } & Partial>; + let children: (Record | HTMLElement)[] | undefined; + + if (Array.isArray(args[0])) { + attributes = {}; + children = args[0]; + } else { + attributes = args[0] as any || {}; + children = args[1]; + } + + const match = H_REGEX.exec(tag); + + if (!match || !match.groups) { + throw new Error('Bad use of h'); + } + + const tagName = match.groups['tag'] || 'div'; + const el = document.createElementNS('http://www.w3.org/2000/svg', tagName) as any as HTMLElement; + + if (match.groups['id']) { + el.id = match.groups['id']; + } + + const classNames = []; + if (match.groups['class']) { + for (const className of match.groups['class'].split('.')) { + if (className !== '') { + classNames.push(className); + } + } + } + if (attributes.className !== undefined) { + for (const className of attributes.className.split('.')) { + if (className !== '') { + classNames.push(className); + } + } + } + if (classNames.length > 0) { + el.className = classNames.join(' '); + } + + const result: Record = {}; + + if (match.groups['name']) { + result[match.groups['name']] = el; + } + + if (children) { + for (const c of children) { + if (isHTMLElement(c)) { + el.appendChild(c); + } else if (typeof c === 'string') { + el.append(c); + } else if ('root' in c) { + Object.assign(result, c); + el.appendChild(c.root); + } + } + } + + for (const [key, value] of Object.entries(attributes)) { + if (key === 'className') { + continue; + } else if (key === 'style') { + for (const [cssKey, cssValue] of Object.entries(value)) { + el.style.setProperty( + camelCaseToHyphenCase(cssKey), + typeof cssValue === 'number' ? cssValue + 'px' : '' + cssValue + ); + } + } else if (key === 'tabIndex') { + el.tabIndex = value; + } else { + el.setAttribute(camelCaseToHyphenCase(key), value.toString()); + } + } + + result['root'] = el; + + return result; +} + function camelCaseToHyphenCase(str: string) { return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } @@ -2408,3 +2510,53 @@ export function trackAttributes(from: Element, to: Element, filter?: string[]): return disposables; } + +/** + * Helper for calculating the "safe triangle" occluded by hovers to avoid early dismissal. + * @see https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles/ for example + */ +export class SafeTriangle { + // 4 triangles, 2 points (x, y) stored for each + private triangles: number[] = []; + + constructor( + private readonly originX: number, + private readonly originY: number, + target: HTMLElement + ) { + const { top, left, right, bottom } = target.getBoundingClientRect(); + const t = this.triangles; + let i = 0; + + t[i++] = left; + t[i++] = top; + t[i++] = right; + t[i++] = top; + + t[i++] = left; + t[i++] = top; + t[i++] = left; + t[i++] = bottom; + + t[i++] = right; + t[i++] = top; + t[i++] = right; + t[i++] = bottom; + + t[i++] = left; + t[i++] = bottom; + t[i++] = right; + t[i++] = bottom; + } + + public contains(x: number, y: number) { + const { triangles, originX, originY } = this; + for (let i = 0; i < 4; i++) { + if (isPointWithinTriangle(x, y, originX, originY, triangles[2 * i], triangles[2 * i + 1], triangles[2 * i + 2], triangles[2 * i + 3])) { + return true; + } + } + + return false; + } +} diff --git a/src/vs/base/browser/domObservable.ts b/src/vs/base/browser/domObservable.ts new file mode 100644 index 00000000000..dd20637727b --- /dev/null +++ b/src/vs/base/browser/domObservable.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createStyleSheet2 } from 'vs/base/browser/dom'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { autorun, IObservable } from 'vs/base/common/observable'; + +export function createStyleSheetFromObservable(css: IObservable): IDisposable { + const store = new DisposableStore(); + const w = store.add(createStyleSheet2()); + store.add(autorun(reader => { + w.setStyle(css.read(reader)); + })); + return store; +} diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index b1a304845a3..265a57113f7 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -427,7 +427,7 @@ function sanitizeRenderedMarkdown( if (element.attributes.getNamedItem('type')?.value === 'checkbox') { element.setAttribute('disabled', ''); } else if (!options.replaceWithPlaintext) { - element.parentElement?.removeChild(element); + element.remove(); } } @@ -482,6 +482,7 @@ export const allowedMarkdownAttr = [ 'alt', 'checked', 'class', + 'colspan', 'controls', 'data-code', 'data-href', @@ -493,6 +494,7 @@ export const allowedMarkdownAttr = [ 'muted', 'playsinline', 'poster', + 'rowspan', 'src', 'style', 'target', @@ -634,7 +636,7 @@ const plainTextRenderer = new Lazy((withCodeBlocks?: boolean) = const plainTextWithCodeBlocksRenderer = new Lazy(() => { const renderer = createRenderer(); renderer.code = (code: string): string => { - return '\n' + '```' + code + '```' + '\n'; + return `\n\`\`\`\n${code}\n\`\`\`\n`; }; return renderer; }); @@ -766,8 +768,8 @@ function completeListItemPattern(list: marked.Tokens.List): marked.Tokens.List | const previousListItemsText = mergeRawTokenText(list.items.slice(0, -1)); - // Grabbing the `- ` or `1. ` off the list item because I can't find a better way to do this - const lastListItemLead = lastListItem.raw.match(/^(\s*(-|\d+\.) +)/)?.[0]; + // Grabbing the `- ` or `1. ` or `* ` off the list item because I can't find a better way to do this + const lastListItemLead = lastListItem.raw.match(/^(\s*(-|\d+\.|\*) +)/)?.[0]; if (!lastListItemLead) { // Is badly formatted return; diff --git a/src/vs/base/browser/trustedTypes.ts b/src/vs/base/browser/trustedTypes.ts index 48c02ca8c97..0ef4b084528 100644 --- a/src/vs/base/browser/trustedTypes.ts +++ b/src/vs/base/browser/trustedTypes.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { mainWindow } from 'vs/base/browser/window'; import { onUnexpectedError } from 'vs/base/common/errors'; export function createTrustedTypesPolicy( @@ -28,7 +27,7 @@ export function createTrustedTypesPolicy= 0 && index < this.viewItems.length) { - this.actionsList.removeChild(this.actionsList.childNodes[index]); + this.actionsList.childNodes[index].remove(); this.viewItemDisposables.deleteAndDispose(this.viewItems[index]); dispose(this.viewItems.splice(index, 1)); this.refreshRole(); diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 3c42632c500..30f588f4d49 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -22,7 +22,7 @@ import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecyc import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./button'; import { localize } from 'vs/nls'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { IActionProvider } from 'vs/base/browser/ui/dropdown/dropdown'; @@ -80,7 +80,7 @@ export class Button extends Disposable implements IButton { protected _label: string | IMarkdownString = ''; protected _labelElement: HTMLElement | undefined; protected _labelShortElement: HTMLElement | undefined; - private _hover: IUpdatableHover | undefined; + private _hover: IManagedHover | undefined; private _onDidClick = this._register(new Emitter()); get onDidClick(): BaseEvent { return this._onDidClick.event; } @@ -306,7 +306,7 @@ export class Button extends Disposable implements IButton { setTitle(title: string) { if (!this._hover && title !== '') { - this._hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this._element, title)); + this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this._element, title)); } else if (this._hover) { this._hover.update(title); } @@ -370,7 +370,7 @@ export class ButtonWithDropdown extends Disposable implements IButton { this.separator.style.backgroundColor = options.buttonSeparator ?? ''; this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportIcons: true })); - this._register(getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.dropdownButton.element, localize("button dropdown more actions", 'More Actions...'))); + this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.dropdownButton.element, localize("button dropdown more actions", 'More Actions...'))); this.dropdownButton.element.setAttribute('aria-haspopup', 'true'); this.dropdownButton.element.setAttribute('aria-expanded', 'false'); this.dropdownButton.element.classList.add('monaco-dropdown-button'); diff --git a/src/vs/base/browser/ui/centered/centeredViewLayout.ts b/src/vs/base/browser/ui/centered/centeredViewLayout.ts index db13907259a..b6bc80b1d99 100644 --- a/src/vs/base/browser/ui/centered/centeredViewLayout.ts +++ b/src/vs/base/browser/ui/centered/centeredViewLayout.ts @@ -166,7 +166,7 @@ export class CenteredViewLayout implements IDisposable { } if (active) { - this.container.removeChild(this.view.element); + this.view.element.remove(); this.splitView = new SplitView(this.container, { inverseAltBehavior: true, orientation: Orientation.HORIZONTAL, @@ -195,9 +195,7 @@ export class CenteredViewLayout implements IDisposable { this.resizeSplitViews(); } else { - if (this.splitView) { - this.container.removeChild(this.splitView.el); - } + this.splitView?.el.remove(); this.splitViewDisposables.clear(); this.splitView?.dispose(); this.splitView = undefined; diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index 4f07df5b8f9..1edda5b714c 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -169,13 +169,11 @@ export class ContextView extends Disposable { if (this.container) { this.toDisposeOnSetContainer.dispose(); + this.view.remove(); if (this.shadowRoot) { - this.shadowRoot.removeChild(this.view); this.shadowRoot = null; this.shadowRootHostElement?.remove(); this.shadowRootHostElement = null; - } else { - this.container.removeChild(this.view); } this.container = null; diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index 1089d8275ba..ba003576019 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -8,7 +8,7 @@ import { $, addDisposableListener, append, EventHelper, EventType, isMouseEvent import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as GestureEventType, Gesture } from 'vs/base/browser/touch'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IMenuOptions } from 'vs/base/browser/ui/menu/menu'; @@ -37,7 +37,7 @@ class BaseDropdown extends ActionRunner { private _onDidChangeVisibility = this._register(new Emitter()); readonly onDidChangeVisibility = this._onDidChangeVisibility.event; - private hover: IUpdatableHover | undefined; + private hover: IManagedHover | undefined; constructor(container: HTMLElement, options: IBaseDropdownOptions) { super(); @@ -107,7 +107,7 @@ class BaseDropdown extends ActionRunner { set tooltip(tooltip: string) { if (this._label) { if (!this.hover && tooltip !== '') { - this.hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this._label, tooltip)); + this.hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this._label, tooltip)); } else if (this.hover) { this.hover.update(tooltip); } diff --git a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index 18cfd87d2bc..007e1de2a66 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -93,7 +93,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { this.element.setAttribute('aria-haspopup', 'true'); this.element.setAttribute('aria-expanded', 'false'); if (this._action.label) { - this._register(getBaseLayerHoverDelegate().setupUpdatableHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.element, this._action.label)); + this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.element, this._action.label)); } this.element.ariaLabel = this._action.label || ''; diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 1d7eacf9fdc..17b24550622 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -1063,7 +1063,7 @@ export class GridView implements IDisposable { const oldRoot = this._root; if (oldRoot) { - this.element.removeChild(oldRoot.element); + oldRoot.element.remove(); oldRoot.dispose(); } @@ -1831,6 +1831,6 @@ export class GridView implements IDisposable { dispose(): void { this.onDidSashResetRelay.dispose(); this.root.dispose(); - this.element.parentElement?.removeChild(this.element); + this.element.remove(); } } diff --git a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts index 724075adb87..83b0c26bcae 100644 --- a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts +++ b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; @@ -43,7 +43,7 @@ export class HighlightedLabel extends Disposable { private highlights: readonly IHighlight[] = []; private supportIcons: boolean; private didEverRender: boolean = false; - private customHover: IUpdatableHover | undefined; + private customHover: IManagedHover | undefined; /** * Create a new {@link HighlightedLabel}. @@ -141,7 +141,7 @@ export class HighlightedLabel extends Disposable { } else { if (!this.customHover && this.title !== '') { const hoverDelegate = this.options?.hoverDelegate ?? getDefaultHoverDelegate('mouse'); - this.customHover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(hoverDelegate, this.domNode, this.title)); + this.customHover = this._register(getBaseLayerHoverDelegate().setupManagedHover(hoverDelegate, this.domNode, this.title)); } else if (this.customHover) { this.customHover.update(this.title); } diff --git a/src/vs/base/browser/ui/hover/hover.ts b/src/vs/base/browser/ui/hover/hover.ts index f2b7582d7fa..f66ba1ea673 100644 --- a/src/vs/base/browser/ui/hover/hover.ts +++ b/src/vs/base/browser/ui/hover/hover.ts @@ -14,11 +14,12 @@ import type { IDisposable } from 'vs/base/common/lifecycle'; */ export interface IHoverDelegate2 { /** - * Shows a hover, provided a hover with the same options object is not already visible. + * Shows a hover, provided a hover with the same {@link options} object is not already visible. + * * @param options A set of options defining the characteristics of the hover. * @param focus Whether to focus the hover (useful for keyboard accessibility). * - * **Example:** A simple usage with a single element target. + * @example A simple usage with a single element target. * * ```typescript * showHover({ @@ -27,7 +28,10 @@ export interface IHoverDelegate2 { * }); * ``` */ - showHover(options: IHoverOptions, focus?: boolean): IHoverWidget | undefined; + showHover( + options: IHoverOptions, + focus?: boolean + ): IHoverWidget | undefined; /** * Hides the hover if it was visible. This call will be ignored if the the hover is currently @@ -41,16 +45,37 @@ export interface IHoverDelegate2 { */ showAndFocusLastHover(): void; - // TODO: Change hoverDelegate arg to exclude the actual delegate and instead use the new options - setupUpdatableHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, content: IUpdatableHoverContentOrFactory, options?: IUpdatableHoverOptions): IUpdatableHover; + /** + * Sets up a managed hover for the given element. A managed hover will set up listeners for + * mouse events, show the hover after a delay and provide hooks to easily update the content. + * + * This should be used over {@link showHover} when fine-grained control is not needed. The + * managed hover also does not scale well, consider using {@link showHover} when showing hovers + * for many elements. + * + * @param hoverDelegate The hover delegate containing hooks and configuration for the hover. + * @param targetElement The target element to show the hover for. + * @param content The content of the hover or a factory that creates it at the time it's shown. + * @param options Additional options for the managed hover. + */ + // TODO: The hoverDelegate parameter should be removed in favor of just a set of options. This + // will avoid confusion around IHoverDelegate/IHoverDelegate2 as well as align more with + // the design of the hover service. + // TODO: Align prototype closer to showHover, deriving options from IHoverOptions if possible. + setupManagedHover(hoverDelegate: IHoverDelegate, targetElement: HTMLElement, content: IManagedHoverContentOrFactory, options?: IManagedHoverOptions): IManagedHover; /** * Shows the hover for the given element if one has been setup. + * + * @param targetElement The target element of the hover, as set up in {@link setupManagedHover}. */ - triggerUpdatableHover(htmlElement: HTMLElement): void; + showManagedHover(targetElement: HTMLElement): void; } export interface IHoverWidget extends IDisposable { + /** + * Whether the hover widget has been disposed. + */ readonly isDisposed: boolean; } @@ -229,33 +254,29 @@ export interface IHoverTarget extends IDisposable { * An optional absolute x coordinate to position the hover with, for example to position the * hover using `MouseEvent.pageX`. */ - x?: number; + readonly x?: number; /** * An optional absolute y coordinate to position the hover with, for example to position the * hover using `MouseEvent.pageY`. */ - y?: number; + readonly y?: number; } -// #region Updatable hover +// #region Managed hover -export interface IUpdatableHoverTooltipMarkdownString { +export interface IManagedHoverTooltipMarkdownString { markdown: IMarkdownString | string | undefined | ((token: CancellationToken) => Promise); markdownNotSupportedFallback: string | undefined; } -export type IUpdatableHoverContent = string | IUpdatableHoverTooltipMarkdownString | HTMLElement | undefined; -export type IUpdatableHoverContentOrFactory = IUpdatableHoverContent | (() => IUpdatableHoverContent); +export type IManagedHoverContent = string | IManagedHoverTooltipMarkdownString | HTMLElement | undefined; +export type IManagedHoverContentOrFactory = IManagedHoverContent | (() => IManagedHoverContent); -export interface IUpdatableHoverOptions { - actions?: IHoverAction[]; - linkHandler?(url: string): void; - trapFocus?: boolean; +export interface IManagedHoverOptions extends Pick { } -export interface IUpdatableHover extends IDisposable { - +export interface IManagedHover extends IDisposable { /** * Allows to programmatically open the hover. */ @@ -269,7 +290,7 @@ export interface IUpdatableHover extends IDisposable { /** * Updates the contents of the hover. */ - update(tooltip: IUpdatableHoverContent, options?: IUpdatableHoverOptions): void; + update(tooltip: IManagedHoverContent, options?: IManagedHoverOptions): void; } -// #endregion Updatable hover +// #endregion Managed hover diff --git a/src/vs/base/browser/ui/hover/hoverDelegate.ts b/src/vs/base/browser/ui/hover/hoverDelegate.ts index d2f1d7884ff..47ea1b77531 100644 --- a/src/vs/base/browser/ui/hover/hoverDelegate.ts +++ b/src/vs/base/browser/ui/hover/hoverDelegate.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IHoverWidget, IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover'; +import type { IHoverWidget, IManagedHoverOptions } from 'vs/base/browser/ui/hover/hover'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -13,7 +13,7 @@ export interface IHoverDelegateTarget extends IDisposable { x?: number; } -export interface IHoverDelegateOptions extends IUpdatableHoverOptions { +export interface IHoverDelegateOptions extends IManagedHoverOptions { /** * The content to display in the primary section of the hover. The type of text determines the * default `hideOnHover` behavior. diff --git a/src/vs/base/browser/ui/hover/hoverDelegate2.ts b/src/vs/base/browser/ui/hover/hoverDelegate2.ts index 13a379222c1..1d6a312f70d 100644 --- a/src/vs/base/browser/ui/hover/hoverDelegate2.ts +++ b/src/vs/base/browser/ui/hover/hoverDelegate2.ts @@ -9,8 +9,8 @@ let baseHoverDelegate: IHoverDelegate2 = { showHover: () => undefined, hideHover: () => undefined, showAndFocusLastHover: () => undefined, - setupUpdatableHover: () => null!, - triggerUpdatableHover: () => undefined + setupManagedHover: () => null!, + showManagedHover: () => undefined }; /** diff --git a/src/vs/base/browser/ui/hover/hoverWidget.css b/src/vs/base/browser/ui/hover/hoverWidget.css index a1d72c2e581..a1b3b43211e 100644 --- a/src/vs/base/browser/ui/hover/hoverWidget.css +++ b/src/vs/base/browser/ui/hover/hoverWidget.css @@ -134,6 +134,11 @@ padding-right: 4px; } +.monaco-hover .hover-row.status-bar .actions .action-container a { + color: var(--vscode-textLink-foreground); + text-decoration: var(--text-link-decoration); +} + .monaco-hover .markdown-hover .hover-contents .codicon { color: inherit; font-size: inherit; diff --git a/src/vs/base/browser/ui/hover/hoverWidget.ts b/src/vs/base/browser/ui/hover/hoverWidget.ts index 2e9ecbdd1fe..9d836121267 100644 --- a/src/vs/base/browser/ui/hover/hoverWidget.ts +++ b/src/vs/base/browser/ui/hover/hoverWidget.ts @@ -50,12 +50,18 @@ export class HoverAction extends Disposable { return new HoverAction(parent, actionOptions, keybindingLabel); } + public readonly actionLabel: string; + public readonly actionKeybindingLabel: string | null; + private readonly actionContainer: HTMLElement; private readonly action: HTMLElement; private constructor(parent: HTMLElement, actionOptions: { label: string; iconClass?: string; run: (target: HTMLElement) => void; commandId: string }, keybindingLabel: string | null) { super(); + this.actionLabel = actionOptions.label; + this.actionKeybindingLabel = keybindingLabel; + this.actionContainer = dom.append(parent, $('div.action-container')); this.actionContainer.setAttribute('tabindex', '0'); diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 214af03716c..e761b71228e 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -12,7 +12,7 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { equals } from 'vs/base/common/objects'; import { Range } from 'vs/base/common/range'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import type { IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { isString } from 'vs/base/common/types'; import { stripIcons } from 'vs/base/common/iconLabels'; @@ -26,8 +26,8 @@ export interface IIconLabelCreationOptions { } export interface IIconLabelValueOptions { - title?: string | IUpdatableHoverTooltipMarkdownString; - descriptionTitle?: string | IUpdatableHoverTooltipMarkdownString; + title?: string | IManagedHoverTooltipMarkdownString; + descriptionTitle?: string | IManagedHoverTooltipMarkdownString; suffix?: string; hideIcon?: boolean; extraClasses?: readonly string[]; @@ -194,7 +194,7 @@ export class IconLabel extends Disposable { } } - private setupHover(htmlElement: HTMLElement, tooltip: string | IUpdatableHoverTooltipMarkdownString | undefined): void { + private setupHover(htmlElement: HTMLElement, tooltip: string | IManagedHoverTooltipMarkdownString | undefined): void { const previousCustomHover = this.customHovers.get(htmlElement); if (previousCustomHover) { previousCustomHover.dispose(); @@ -207,7 +207,7 @@ export class IconLabel extends Disposable { } if (this.hoverDelegate.showNativeHover) { - function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IUpdatableHoverTooltipMarkdownString | undefined): void { + function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IManagedHoverTooltipMarkdownString | undefined): void { if (isString(tooltip)) { // Icons don't render in the native hover so we strip them out htmlElement.title = stripIcons(tooltip); @@ -219,7 +219,7 @@ export class IconLabel extends Disposable { } setupNativeHover(htmlElement, tooltip); } else { - const hoverDisposable = getBaseLayerHoverDelegate().setupUpdatableHover(this.hoverDelegate, htmlElement, tooltip); + const hoverDisposable = getBaseLayerHoverDelegate().setupManagedHover(this.hoverDelegate, htmlElement, tooltip); if (hoverDisposable) { this.customHovers.set(htmlElement, hoverDisposable); } diff --git a/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts b/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts index 6f960b8add0..ea8179ad642 100644 --- a/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { reset } from 'vs/base/browser/dom'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; @@ -12,7 +12,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; export class SimpleIconLabel implements IDisposable { - private hover?: IUpdatableHover; + private hover?: IManagedHover; constructor( private readonly _container: HTMLElement @@ -24,7 +24,7 @@ export class SimpleIconLabel implements IDisposable { set title(title: string) { if (!this.hover && title) { - this.hover = getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this._container, title); + this.hover = getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this._container, title); } else if (this.hover) { this.hover.update(title); } diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index e4215ad7642..870afb65c0a 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -11,7 +11,7 @@ import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -114,7 +114,7 @@ export class InputBox extends Widget { private cachedContentHeight: number | undefined; private maxHeight: number = Number.POSITIVE_INFINITY; private scrollableElement: ScrollableElement | undefined; - private hover: IUpdatableHover | undefined; + private hover: IManagedHover | undefined; private _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; @@ -235,7 +235,7 @@ export class InputBox extends Widget { public setTooltip(tooltip: string): void { this.tooltip = tooltip; if (!this.hover) { - this.hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.input, tooltip)); + this.hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.input, tooltip)); } else { this.hover.update(tooltip); } diff --git a/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts b/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts index b6c8e1e4db1..189317b48e8 100644 --- a/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts +++ b/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { UILabelProvider } from 'vs/base/common/keybindingLabels'; @@ -61,7 +61,7 @@ export class KeybindingLabel extends Disposable { private readonly keyElements = new Set(); - private hover: IUpdatableHover; + private hover: IManagedHover; private keybinding: ResolvedKeybinding | undefined; private matches: Matches | undefined; private didEverRender: boolean; @@ -78,7 +78,7 @@ export class KeybindingLabel extends Disposable { this.domNode.style.color = labelForeground; } - this.hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.domNode, '')); + this.hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.domNode, '')); this.didEverRender = false; container.appendChild(this.domNode); diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 005d34d6738..276cba0d2c6 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -1158,7 +1158,7 @@ export class ListView implements IListView { const container = getDragImageContainer(this.domNode); container.appendChild(dragImage); event.dataTransfer.setDragImage(dragImage, -10, -10); - setTimeout(() => container.removeChild(dragImage), 0); + setTimeout(() => dragImage.remove(), 0); } this.domNode.classList.add('dragging'); @@ -1542,7 +1542,7 @@ export class ListView implements IListView { this.virtualDelegate.setDynamicHeight?.(item.element, item.size); item.lastDynamicHeightWidth = this.renderWidth; - this.rowsContainer.removeChild(row.domNode); + row.domNode.remove(); this.cache.release(row); return item.size - size; @@ -1570,9 +1570,7 @@ export class ListView implements IListView { this.items = []; - if (this.domNode && this.domNode.parentNode) { - this.domNode.parentNode.removeChild(this.domNode); - } + this.domNode?.remove(); this.dragOverAnimationDisposable?.dispose(); this.disposables.dispose(); diff --git a/src/vs/base/browser/ui/list/rowCache.ts b/src/vs/base/browser/ui/list/rowCache.ts index f71bdfd01f3..ff605b097ce 100644 --- a/src/vs/base/browser/ui/list/rowCache.ts +++ b/src/vs/base/browser/ui/list/rowCache.ts @@ -13,14 +13,6 @@ export interface IRow { templateData: any; } -function removeFromParent(element: HTMLElement): void { - try { - element.parentElement?.removeChild(element); - } catch (e) { - // this will throw if this happens due to a blur event, nasty business - } -} - export class RowCache implements IDisposable { private cache = new Map(); @@ -104,7 +96,7 @@ export class RowCache implements IDisposable { private doRemoveNode(domNode: HTMLElement) { domNode.classList.remove('scrolling'); - removeFromParent(domNode); + domNode.remove(); } private getTemplateCache(templateId: string): IRow[] { diff --git a/src/vs/base/browser/ui/sash/sash.ts b/src/vs/base/browser/ui/sash/sash.ts index dfacef7d6e2..210b98af009 100644 --- a/src/vs/base/browser/ui/sash/sash.ts +++ b/src/vs/base/browser/ui/sash/sash.ts @@ -575,7 +575,7 @@ export class Sash extends Disposable { const onPointerUp = (e: PointerEvent) => { EventHelper.stop(e, false); - this.el.removeChild(style); + style.remove(); this.el.classList.remove('active'); this._onDidEnd.fire(); diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index a58782d95df..64bdfa22cb4 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -9,7 +9,7 @@ import { IContentActionHandler } from 'vs/base/browser/formattedTextRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { AnchorPosition, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IListEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; @@ -104,7 +104,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi private selectionDetailsPane!: HTMLElement; private _skipLayout: boolean = false; private _cachedMaxDetailsHeight?: number; - private _hover?: IUpdatableHover; + private _hover?: IManagedHover; private _sticky: boolean = false; // for dev purposes only @@ -153,7 +153,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi private setTitle(title: string): void { if (!this._hover && title) { - this._hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.selectElement, title)); + this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.selectElement, title)); } else if (this._hover) { this._hover.update(title); } @@ -520,12 +520,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi return { dispose: () => { // contextView will dispose itself if moving from one View to another - try { - container.removeChild(this.selectDropDownContainer); // remove to take out the CSS rules we add - } - catch (error) { - // Ignore, removed already by change of focus - } + this.selectDropDownContainer.remove(); // remove to take out the CSS rules we add } }; } @@ -612,8 +607,8 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi && this.options.length > maxVisibleOptionsBelow ) { this._dropDownPosition = AnchorPosition.ABOVE; - this.selectDropDownContainer.removeChild(this.selectDropDownListContainer); - this.selectDropDownContainer.removeChild(this.selectionDetailsPane); + this.selectDropDownListContainer.remove(); + this.selectionDetailsPane.remove(); this.selectDropDownContainer.appendChild(this.selectionDetailsPane); this.selectDropDownContainer.appendChild(this.selectDropDownListContainer); @@ -622,8 +617,8 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi } else { this._dropDownPosition = AnchorPosition.BELOW; - this.selectDropDownContainer.removeChild(this.selectDropDownListContainer); - this.selectDropDownContainer.removeChild(this.selectionDetailsPane); + this.selectDropDownListContainer.remove(); + this.selectionDetailsPane.remove(); this.selectDropDownContainer.appendChild(this.selectDropDownListContainer); this.selectDropDownContainer.appendChild(this.selectionDetailsPane); @@ -879,7 +874,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi const tagName = child.tagName && child.tagName.toLowerCase(); if (tagName === 'img') { - element.removeChild(child); + child.remove(); } else { cleanRenderedMarkdown(child); } diff --git a/src/vs/base/browser/ui/splitview/paneview.ts b/src/vs/base/browser/ui/splitview/paneview.ts index a934addd1fb..6a78dee3860 100644 --- a/src/vs/base/browser/ui/splitview/paneview.ts +++ b/src/vs/base/browser/ui/splitview/paneview.ts @@ -382,7 +382,7 @@ class PaneDraggable extends Disposable { const dragImage = append(this.pane.element.ownerDocument.body, $('.monaco-drag-image', {}, this.pane.draggableElement.textContent || '')); e.dataTransfer.setDragImage(dragImage, -10, -10); - setTimeout(() => this.pane.element.ownerDocument.body.removeChild(dragImage), 0); + setTimeout(() => dragImage.remove(), 0); this.context.draggable = this; } diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index ae7ad6708e4..ac966adf435 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -1127,7 +1127,7 @@ export class SplitView this.onViewChange(item, size)); - const containerDisposable = toDisposable(() => this.viewContainer.removeChild(container)); + const containerDisposable = toDisposable(() => container.remove()); const disposable = combinedDisposable(onChangeDisposable, containerDisposable); let viewSize: ViewItemSize; diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 631c0015d4b..b2c8959c3c5 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -134,7 +134,7 @@ class ColumnHeader extends Disposable implements IView { this.element = $('.monaco-table-th', { 'data-col-index': index }, column.label); if (column.tooltip) { - this._register(getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.element, column.tooltip)); + this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, column.tooltip)); } } diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index a52c00287d2..c141f381fb3 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -15,7 +15,7 @@ import 'vs/css!./toggle'; import { isActiveElement, $, addDisposableListener, EventType } from 'vs/base/browser/dom'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; export interface IToggleOpts extends IToggleStyles { @@ -113,7 +113,7 @@ export class Toggle extends Widget { readonly domNode: HTMLElement; private _checked: boolean; - private _hover: IUpdatableHover; + private _hover: IManagedHover; constructor(opts: IToggleOpts) { super(); @@ -134,7 +134,7 @@ export class Toggle extends Widget { } this.domNode = document.createElement('div'); - this._hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(opts.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.domNode, this._opts.title)); + this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(opts.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.domNode, this._opts.title)); this.domNode.classList.add(...classes); if (!this._opts.notFocusable) { this.domNode.tabIndex = 0; diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index 57aac5edb41..ce42a2a9ac7 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -38,6 +38,16 @@ export interface IToolBarOptions { * If true, toggled primary items are highlighted with a background color. */ highlightToggledItems?: boolean; + + /** + * Render action with icons (default: `true`) + */ + icon?: boolean; + + /** + * Render action with label (default: `false`) + */ + label?: boolean; } /** @@ -50,7 +60,6 @@ export class ToolBar extends Disposable { private toggleMenuActionViewItem: DropdownMenuActionViewItem | undefined; private submenuActionViewItems: DropdownMenuActionViewItem[] = []; private hasSecondaryActions: boolean = false; - private readonly lookupKeybindings: boolean; private readonly element: HTMLElement; private _onDidChangeDropdownVisibility = this._register(new EventMultiplexer()); @@ -62,7 +71,6 @@ export class ToolBar extends Disposable { options.hoverDelegate = options.hoverDelegate ?? this._register(createInstantHoverDelegate()); this.options = options; - this.lookupKeybindings = typeof this.options.getKeyBinding === 'function'; this.toggleMenuAction = this._register(new ToggleMenuAction(() => this.toggleMenuActionViewItem?.show(), options.toggleMenuTitle)); @@ -198,7 +206,7 @@ export class ToolBar extends Disposable { } primaryActionsToSet.forEach(action => { - this.actionBar.push(action, { icon: true, label: false, keybinding: this.getKeybindingLabel(action) }); + this.actionBar.push(action, { icon: this.options.icon ?? true, label: this.options.label ?? false, keybinding: this.getKeybindingLabel(action) }); }); } @@ -207,7 +215,7 @@ export class ToolBar extends Disposable { } private getKeybindingLabel(action: IAction): string | undefined { - const key = this.lookupKeybindings ? this.options.getKeyBinding?.(action) : undefined; + const key = this.options.getKeyBinding?.(action); return key?.getLabel() ?? undefined; } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 7868ee55f6b..250efbc4296 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -796,7 +796,7 @@ class FindWidget extends Disposable { super(); container.appendChild(this.elements.root); - this._register(toDisposable(() => container.removeChild(this.elements.root))); + this._register(toDisposable(() => this.elements.root.remove())); const styles = options?.styles ?? unthemedFindWidgetStyles; diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index eff4c99914a..68488a9b4bd 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -569,10 +569,6 @@ export class AsyncDataTree implements IDisposable this.tree.resort(this.getDataNode(element), recursive); } - hasElement(element: TInput | T): boolean { - return this.tree.hasElement(this.getDataNode(element)); - } - hasNode(element: TInput | T): boolean { return element === this.root.element || this.nodes.has(element as T); } diff --git a/src/vs/base/common/collections.ts b/src/vs/base/common/collections.ts index 0b306144e5e..d0df190c75b 100644 --- a/src/vs/base/common/collections.ts +++ b/src/vs/base/common/collections.ts @@ -80,3 +80,61 @@ export function intersection(setA: Set, setB: Iterable): Set { } return result; } + +export class SetWithKey implements Set { + private _map = new Map(); + + constructor(values: T[], private toKey: (t: T) => any) { + for (const value of values) { + this.add(value); + } + } + + get size(): number { + return this._map.size; + } + + add(value: T): this { + const key = this.toKey(value); + this._map.set(key, value); + return this; + } + + delete(value: T): boolean { + return this._map.delete(this.toKey(value)); + } + + has(value: T): boolean { + return this._map.has(this.toKey(value)); + } + + *entries(): IterableIterator<[T, T]> { + for (const entry of this._map.values()) { + yield [entry, entry]; + } + } + + keys(): IterableIterator { + return this.values(); + } + + *values(): IterableIterator { + for (const entry of this._map.values()) { + yield entry; + } + } + + clear(): void { + this._map.clear(); + } + + forEach(callbackfn: (value: T, value2: T, set: Set) => void, thisArg?: any): void { + this._map.forEach(entry => callbackfn.call(thisArg, entry, entry, this)); + } + + [Symbol.iterator](): IterableIterator { + return this.values(); + } + + [Symbol.toStringTag]: string = 'SetWithKey'; +} diff --git a/src/vs/base/common/equals.ts b/src/vs/base/common/equals.ts index 22825c59d9e..6e2ae8503ab 100644 --- a/src/vs/base/common/equals.ts +++ b/src/vs/base/common/equals.ts @@ -6,6 +6,10 @@ import * as arrays from 'vs/base/common/arrays'; export type EqualityComparer = (a: T, b: T) => boolean; + +/** + * Compares two items for equality using strict equality. +*/ export const strictEquals: EqualityComparer = (a, b) => a === b; /** @@ -30,11 +34,30 @@ export function itemEquals(): EqualityC return (a, b) => a.equals(b); } -export function equalsIfDefined(v1: T | undefined, v2: T | undefined, equals: EqualityComparer): boolean { - if (!v1 || !v2) { - return v1 === v2; +/** + * Checks if two items are both null or undefined, or are equal according to the provided equality comparer. +*/ +export function equalsIfDefined(v1: T | undefined | null, v2: T | undefined | null, equals: EqualityComparer): boolean; +/** + * Returns an equality comparer that checks if two items are both null or undefined, or are equal according to the provided equality comparer. +*/ +export function equalsIfDefined(equals: EqualityComparer): EqualityComparer; +export function equalsIfDefined(equalsOrV1: EqualityComparer | T, v2?: T | undefined | null, equals?: EqualityComparer): EqualityComparer | boolean { + if (equals !== undefined) { + const v1 = equalsOrV1 as T | undefined; + if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) { + return v2 === v1; + } + return equals(v1, v2); + } else { + const equals = equalsOrV1 as EqualityComparer; + return (v1, v2) => { + if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) { + return v2 === v1; + } + return equals(v1, v2); + }; } - return equals(v1, v2); } /** diff --git a/src/vs/base/common/errors.ts b/src/vs/base/common/errors.ts index f0d9296057b..ce5d8b29852 100644 --- a/src/vs/base/common/errors.ts +++ b/src/vs/base/common/errors.ts @@ -137,6 +137,19 @@ export function transformErrorForSerialization(error: any): any { return error; } +export function transformErrorFromSerialization(data: SerializedError): Error { + let error: Error; + if (data.noTelemetry) { + error = new ErrorNoTelemetry(); + } else { + error = new Error(); + error.name = data.name; + } + error.message = data.message; + error.stack = data.stack; + return error; +} + // see https://github.com/v8/v8/wiki/Stack%20Trace%20API#basic-stack-traces export interface V8CallSite { getThis(): unknown; diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index f94fa3673b6..d563a2c77db 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -831,13 +831,15 @@ export function setGlobalLeakWarningThreshold(n: number): IDisposable { class LeakageMonitor { + private static _idPool = 1; + private _stacks: Map | undefined; private _warnCountdown: number = 0; constructor( private readonly _errorHandler: (err: Error) => void, readonly threshold: number, - readonly name: string = Math.random().toString(18).slice(2, 5), + readonly name: string = (LeakageMonitor._idPool++).toString(16).padStart(3, '0') ) { } dispose(): void { @@ -952,14 +954,26 @@ const forEachListener = (listeners: ListenerOrListeners, fn: (c: ListenerC }; -const _listenerFinalizers = _enableListenerGCedWarning - ? new FinalizationRegistry(heldValue => { +let _listenerFinalizers: FinalizationRegistry | undefined; + +if (_enableListenerGCedWarning) { + const leaks: string[] = []; + + setInterval(() => { + if (leaks.length === 0) { + return; + } + console.warn('[LEAKING LISTENERS] GC\'ed these listeners that were NOT yet disposed:'); + console.warn(leaks.join('\n')); + leaks.length = 0; + }, 3000); + + _listenerFinalizers = new FinalizationRegistry(heldValue => { if (typeof heldValue === 'string') { - console.warn('[LEAKING LISTENER] GC\'ed a listener that was NOT yet disposed. This is where is was created:'); - console.warn(heldValue); + leaks.push(heldValue); } - }) - : undefined; + }); +} /** * The Emitter can be used to expose an Event to the public @@ -1126,8 +1140,9 @@ export class Emitter { } if (_listenerFinalizers) { - const stack = new Error().stack!.split('\n').slice(2).join('\n').trim(); - _listenerFinalizers.register(result, stack, result); + const stack = new Error().stack!.split('\n').slice(2, 3).join('\n').trim(); + const match = /(file:|vscode-file:\/\/vscode-app)?(\/[^:]*:\d+:\d+)/.exec(stack); + _listenerFinalizers.register(result, match?.[2] ?? stack, result); } return result; diff --git a/src/vs/base/common/history.ts b/src/vs/base/common/history.ts index 1a73131ea9a..9569daa7153 100644 --- a/src/vs/base/common/history.ts +++ b/src/vs/base/common/history.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { SetWithKey } from 'vs/base/common/collections'; import { ArrayNavigator, INavigator } from 'vs/base/common/navigator'; export class HistoryNavigator implements INavigator { @@ -114,6 +115,10 @@ interface HistoryNode { next: HistoryNode | undefined; } +/** + * The right way to use HistoryNavigator2 is for the last item in the list to be the user's uncommitted current text. eg empty string, or whatever has been typed. Then + * the user can navigate away from the last item through the list, and back to it. When updating the last item, call replaceLast. + */ export class HistoryNavigator2 { private valueSet: Set; @@ -123,7 +128,7 @@ export class HistoryNavigator2 { private _size: number; get size(): number { return this._size; } - constructor(history: readonly T[], private capacity: number = 10) { + constructor(history: readonly T[], private capacity: number = 10, private identityFn: (t: T) => any = t => t) { if (history.length < 1) { throw new Error('not supported'); } @@ -135,7 +140,7 @@ export class HistoryNavigator2 { next: undefined }; - this.valueSet = new Set([history[0]]); + this.valueSet = new SetWithKey([history[0]], identityFn); for (let i = 1; i < history.length; i++) { this.add(history[i]); } @@ -172,7 +177,7 @@ export class HistoryNavigator2 { * @returns old last value */ replaceLast(value: T): T { - if (this.tail.value === value) { + if (this.identityFn(this.tail.value) === this.identityFn(value)) { return value; } @@ -258,8 +263,9 @@ export class HistoryNavigator2 { private _deleteFromList(value: T): void { let temp = this.head; + const valueKey = this.identityFn(value); while (temp !== this.tail) { - if (temp.value === value) { + if (this.identityFn(temp.value) === valueKey) { if (temp === this.head) { this.head = this.head.next!; this.head.previous = undefined; diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 0bbc2413bb5..c329ed6dc71 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -44,9 +44,10 @@ export namespace Iterable { return iterable[Symbol.iterator]().next().value; } - export function some(iterable: Iterable, predicate: (t: T) => unknown): boolean { + export function some(iterable: Iterable, predicate: (t: T, i: number) => unknown): boolean { + let i = 0; for (const element of iterable) { - if (predicate(element)) { + if (predicate(element, i++)) { return true; } } @@ -82,6 +83,13 @@ export namespace Iterable { } } + export function* flatMap(iterable: Iterable, fn: (t: T, index: number) => Iterable): Iterable { + let index = 0; + for (const element of iterable) { + yield* fn(element, index++); + } + } + export function* concat(...iterables: Iterable[]): Iterable { for (const iterable of iterables) { yield* iterable; diff --git a/src/vs/base/common/json.ts b/src/vs/base/common/json.ts index dadcbaf74f1..e4adc59003e 100644 --- a/src/vs/base/common/json.ts +++ b/src/vs/base/common/json.ts @@ -1308,40 +1308,6 @@ export function visit(text: string, visitor: JSONVisitor, options: ParseOptions return true; } -/** - * Takes JSON with JavaScript-style comments and remove - * them. Optionally replaces every none-newline character - * of comments with a replaceCharacter - */ -export function stripComments(text: string, replaceCh?: string): string { - - const _scanner = createScanner(text); - const parts: string[] = []; - let kind: SyntaxKind; - let offset = 0; - let pos: number; - - do { - pos = _scanner.getPosition(); - kind = _scanner.scan(); - switch (kind) { - case SyntaxKind.LineCommentTrivia: - case SyntaxKind.BlockCommentTrivia: - case SyntaxKind.EOF: - if (offset !== pos) { - parts.push(text.substring(offset, pos)); - } - if (replaceCh !== undefined) { - parts.push(_scanner.getTokenValue().replace(/[^\r\n]/g, replaceCh)); - } - offset = _scanner.getPosition(); - break; - } - } while (kind !== SyntaxKind.EOF); - - return parts.join(''); -} - export function getNodeType(value: any): NodeType { switch (typeof value) { case 'boolean': return 'boolean'; diff --git a/src/vs/base/common/stripComments.d.ts b/src/vs/base/common/jsonc.d.ts similarity index 74% rename from src/vs/base/common/stripComments.d.ts rename to src/vs/base/common/jsonc.d.ts index af5b182b5bf..504e6c60f9f 100644 --- a/src/vs/base/common/stripComments.d.ts +++ b/src/vs/base/common/jsonc.d.ts @@ -3,11 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/** + * A drop-in replacement for JSON.parse that can parse + * JSON with comments and trailing commas. + * + * @param content the content to strip comments from + * @returns the parsed content as JSON +*/ +export function parse(content: string): any; + /** * Strips single and multi line JavaScript comments from JSON * content. Ignores characters in strings BUT doesn't support * string continuation across multiple lines since it is not * supported in JSON. + * * @param content the content to strip comments from * @returns the content without comments */ diff --git a/src/vs/base/common/stripComments.js b/src/vs/base/common/jsonc.js similarity index 78% rename from src/vs/base/common/stripComments.js rename to src/vs/base/common/jsonc.js index c59205e14ab..7d8eacfdc10 100644 --- a/src/vs/base/common/stripComments.js +++ b/src/vs/base/common/jsonc.js @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; +/// //@ts-check +'use strict'; (function () { function factory(path, os, productName, cwd) { @@ -17,7 +18,6 @@ const regexp = /("[^"\\]*(?:\\.[^"\\]*)*")|('[^'\\]*(?:\\.[^'\\]*)*')|(\/\*[^\/\*]*(?:(?:\*|\/)[^\/\*]*)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))|(,\s*[}\]])/g; /** - * * @param {string} content * @returns {string} */ @@ -46,12 +46,27 @@ } }); } + + /** + * @param {string} content + * @returns {any} + */ + function parse(content) { + const commentsStripped = stripComments(content); + + try { + return JSON.parse(commentsStripped); + } catch (error) { + const trailingCommasStriped = commentsStripped.replace(/,\s*([}\]])/g, '$1'); + return JSON.parse(trailingCommasStriped); + } + } return { - stripComments + stripComments, + parse }; } - if (typeof define === 'function') { // amd define([], function () { return factory(); }); @@ -59,6 +74,6 @@ // commonjs module.exports = factory(); } else { - console.trace('strip comments defined in UNKNOWN context (neither requirejs or commonjs)'); + console.trace('jsonc defined in UNKNOWN context (neither requirejs or commonjs)'); } })(); diff --git a/src/vs/base/common/numbers.ts b/src/vs/base/common/numbers.ts index 29e65b86032..ab4c9f92e06 100644 --- a/src/vs/base/common/numbers.ts +++ b/src/vs/base/common/numbers.ts @@ -69,3 +69,30 @@ export class SlidingWindowAverage { return this._val; } } + +/** Returns whether the point is within the triangle formed by the following 6 x/y point pairs */ +export function isPointWithinTriangle( + x: number, y: number, + ax: number, ay: number, + bx: number, by: number, + cx: number, cy: number +) { + const v0x = cx - ax; + const v0y = cy - ay; + const v1x = bx - ax; + const v1y = by - ay; + const v2x = x - ax; + const v2y = y - ay; + + const dot00 = v0x * v0x + v0y * v0y; + const dot01 = v0x * v1x + v0y * v1y; + const dot02 = v0x * v2x + v0y * v2y; + const dot11 = v1x * v1x + v1y * v1y; + const dot12 = v1x * v2x + v1y * v2y; + + const invDenom = 1 / (dot00 * dot11 - dot01 * dot01); + const u = (dot11 * dot02 - dot01 * dot12) * invDenom; + const v = (dot00 * dot12 - dot01 * dot02) * invDenom; + + return u >= 0 && v >= 0 && u + v < 1; +} diff --git a/src/vs/base/common/observable.ts b/src/vs/base/common/observable.ts index a4b21404a1a..c090a272068 100644 --- a/src/vs/base/common/observable.ts +++ b/src/vs/base/common/observable.ts @@ -60,6 +60,9 @@ export { waitForState, derivedWithCancellationToken, } from 'vs/base/common/observableInternal/promise'; +export { + observableValueOpts +} from 'vs/base/common/observableInternal/api'; import { ConsoleObservableLogger, setLogger } from 'vs/base/common/observableInternal/logging'; diff --git a/src/vs/base/common/observableInternal/api.ts b/src/vs/base/common/observableInternal/api.ts new file mode 100644 index 00000000000..6e56671b7e5 --- /dev/null +++ b/src/vs/base/common/observableInternal/api.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EqualityComparer, strictEquals } from 'vs/base/common/equals'; +import { ISettableObservable } from 'vs/base/common/observable'; +import { ObservableValue } from 'vs/base/common/observableInternal/base'; +import { IDebugNameData, DebugNameData } from 'vs/base/common/observableInternal/debugName'; +import { LazyObservableValue } from 'vs/base/common/observableInternal/lazyObservableValue'; + +export function observableValueOpts( + options: IDebugNameData & { + equalsFn?: EqualityComparer; + lazy?: boolean; + }, + initialValue: T +): ISettableObservable { + if (options.lazy) { + return new LazyObservableValue( + new DebugNameData(options.owner, options.debugName, undefined), + initialValue, + options.equalsFn ?? strictEquals, + ); + } + return new ObservableValue( + new DebugNameData(options.owner, options.debugName, undefined), + initialValue, + options.equalsFn ?? strictEquals, + ); +} diff --git a/src/vs/base/common/observableInternal/autorun.ts b/src/vs/base/common/observableInternal/autorun.ts index a2f169ee4d6..845e870d65d 100644 --- a/src/vs/base/common/observableInternal/autorun.ts +++ b/src/vs/base/common/observableInternal/autorun.ts @@ -76,7 +76,7 @@ export function autorunWithStoreHandleChanges( { owner: options.owner, debugName: options.debugName, - debugReferenceFn: options.debugReferenceFn, + debugReferenceFn: options.debugReferenceFn ?? fn, createEmptyChangeSummary: options.createEmptyChangeSummary, handleChange: options.handleChange, }, @@ -154,7 +154,7 @@ export class AutorunObserver implements IObserver, IReader } constructor( - private readonly _debugNameData: DebugNameData, + public readonly _debugNameData: DebugNameData, public readonly _runFn: (reader: IReader, changeSummary: TChangeSummary) => void, private readonly createChangeSummary: (() => TChangeSummary) | undefined, private readonly _handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined, diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index 7f76c8cc1ab..3c63a20116d 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/src/vs/base/common/observableInternal/base.ts @@ -6,7 +6,7 @@ import { strictEquals, EqualityComparer } from 'vs/base/common/equals'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { keepObserved, recomputeInitiallyAndOnChange } from 'vs/base/common/observable'; -import { DebugNameData, IDebugNameData, Owner, getFunctionName } from 'vs/base/common/observableInternal/debugName'; +import { DebugNameData, DebugOwner, getFunctionName } from 'vs/base/common/observableInternal/debugName'; import type { derivedOpts } from 'vs/base/common/observableInternal/derived'; import { getLogger } from 'vs/base/common/observableInternal/logging'; @@ -201,9 +201,9 @@ export abstract class ConvenientObservable implements IObservable(fn: (value: T, reader: IReader) => TNew): IObservable; - public map(owner: Owner, fn: (value: T, reader: IReader) => TNew): IObservable; - public map(fnOrOwner: Owner | ((value: T, reader: IReader) => TNew), fnOrUndefined?: (value: T, reader: IReader) => TNew): IObservable { - const owner = fnOrUndefined === undefined ? undefined : fnOrOwner as Owner; + public map(owner: DebugOwner, fn: (value: T, reader: IReader) => TNew): IObservable; + public map(fnOrOwner: DebugOwner | ((value: T, reader: IReader) => TNew), fnOrUndefined?: (value: T, reader: IReader) => TNew): IObservable { + const owner = fnOrUndefined === undefined ? undefined : fnOrOwner as DebugOwner; const fn = fnOrUndefined === undefined ? fnOrOwner as (value: T, reader: IReader) => TNew : fnOrUndefined; return _derived( @@ -385,19 +385,6 @@ export function observableValue(nameOrOwner: string | object, return new ObservableValue(debugNameData, initialValue, strictEquals); } -export function observableValueOpts( - options: IDebugNameData & { - equalsFn?: EqualityComparer; - }, - initialValue: T -): ISettableObservable { - return new ObservableValue( - new DebugNameData(options.owner, options.debugName, undefined), - initialValue, - options.equalsFn ?? strictEquals, - ); -} - export class ObservableValue extends BaseObservable implements ISettableObservable { diff --git a/src/vs/base/common/observableInternal/debugName.ts b/src/vs/base/common/observableInternal/debugName.ts index 481d24f0377..1ff1f244357 100644 --- a/src/vs/base/common/observableInternal/debugName.ts +++ b/src/vs/base/common/observableInternal/debugName.ts @@ -8,7 +8,7 @@ export interface IDebugNameData { * The owner object of an observable. * Used for debugging only, such as computing a name for the observable by iterating over the fields of the owner. */ - readonly owner?: Owner | undefined; + readonly owner?: DebugOwner | undefined; /** * A string or function that returns a string that represents the name of the observable. @@ -25,7 +25,7 @@ export interface IDebugNameData { export class DebugNameData { constructor( - public readonly owner: Owner | undefined, + public readonly owner: DebugOwner | undefined, public readonly debugNameSource: DebugNameSource | undefined, public readonly referenceFn: Function | undefined, ) { } @@ -36,10 +36,10 @@ export class DebugNameData { } /** - * The owner object of an observable. + * The owning object of an observable. * Is only used for debugging purposes, such as computing a name for the observable by iterating over the fields of the owner. */ -export type Owner = object | undefined; +export type DebugOwner = object | undefined; export type DebugNameSource = string | (() => string | undefined); const countPerName = new Map(); diff --git a/src/vs/base/common/observableInternal/derived.ts b/src/vs/base/common/observableInternal/derived.ts index 9e95bf9dccc..8de22247dbf 100644 --- a/src/vs/base/common/observableInternal/derived.ts +++ b/src/vs/base/common/observableInternal/derived.ts @@ -7,7 +7,7 @@ import { assertFn } from 'vs/base/common/assert'; import { EqualityComparer, strictEquals } from 'vs/base/common/equals'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { BaseObservable, IChangeContext, IObservable, IObserver, IReader, ISettableObservable, ITransaction, _setDerivedOpts, } from 'vs/base/common/observableInternal/base'; -import { DebugNameData, IDebugNameData, Owner } from 'vs/base/common/observableInternal/debugName'; +import { DebugNameData, IDebugNameData, DebugOwner } from 'vs/base/common/observableInternal/debugName'; import { getLogger } from 'vs/base/common/observableInternal/logging'; /** @@ -17,8 +17,8 @@ import { getLogger } from 'vs/base/common/observableInternal/logging'; * {@link computeFn} should start with a JS Doc using `@description` to name the derived. */ export function derived(computeFn: (reader: IReader) => T): IObservable; -export function derived(owner: Owner, computeFn: (reader: IReader) => T): IObservable; -export function derived(computeFnOrOwner: ((reader: IReader) => T) | Owner, computeFn?: ((reader: IReader) => T) | undefined): IObservable { +export function derived(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable; +export function derived(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFn?: ((reader: IReader) => T) | undefined): IObservable { if (computeFn !== undefined) { return new Derived( new DebugNameData(computeFnOrOwner, undefined, computeFn), @@ -39,7 +39,7 @@ export function derived(computeFnOrOwner: ((reader: IReader) => T) | Owner, c ); } -export function derivedWithSetter(owner: Owner | undefined, computeFn: (reader: IReader) => T, setter: (value: T, transaction: ITransaction | undefined) => void): ISettableObservable { +export function derivedWithSetter(owner: DebugOwner | undefined, computeFn: (reader: IReader) => T, setter: (value: T, transaction: ITransaction | undefined) => void): ISettableObservable { return new DerivedWithSetter( new DebugNameData(owner, undefined, computeFn), computeFn, @@ -105,7 +105,7 @@ export function derivedWithStore(computeFn: (reader: IReader, store: Disposab export function derivedWithStore(owner: object, computeFn: (reader: IReader, store: DisposableStore) => T): IObservable; export function derivedWithStore(computeFnOrOwner: ((reader: IReader, store: DisposableStore) => T) | object, computeFnOrUndefined?: ((reader: IReader, store: DisposableStore) => T)): IObservable { let computeFn: (reader: IReader, store: DisposableStore) => T; - let owner: Owner; + let owner: DebugOwner; if (computeFnOrUndefined === undefined) { computeFn = computeFnOrOwner as any; owner = undefined; @@ -128,10 +128,10 @@ export function derivedWithStore(computeFnOrOwner: ((reader: IReader, store: } export function derivedDisposable(computeFn: (reader: IReader) => T): IObservable; -export function derivedDisposable(owner: Owner, computeFn: (reader: IReader) => T): IObservable; -export function derivedDisposable(computeFnOrOwner: ((reader: IReader) => T) | Owner, computeFnOrUndefined?: ((reader: IReader) => T)): IObservable { +export function derivedDisposable(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable; +export function derivedDisposable(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFnOrUndefined?: ((reader: IReader) => T)): IObservable { let computeFn: (reader: IReader) => T; - let owner: Owner; + let owner: DebugOwner; if (computeFnOrUndefined === undefined) { computeFn = computeFnOrOwner as any; owner = undefined; @@ -140,11 +140,15 @@ export function derivedDisposable(computeFnOr computeFn = computeFnOrUndefined as any; } - const store = new DisposableStore(); + let store: DisposableStore | undefined = undefined; return new Derived( new DebugNameData(owner, undefined, computeFn), r => { - store.clear(); + if (!store) { + store = new DisposableStore(); + } else { + store.clear(); + } const result = computeFn(r); if (result) { store.add(result); @@ -152,7 +156,12 @@ export function derivedDisposable(computeFnOr return result; }, undefined, undefined, - () => store.dispose(), + () => { + if (store) { + store.dispose(); + store = undefined; + } + }, strictEquals ); } @@ -192,7 +201,7 @@ export class Derived extends BaseObservable im } constructor( - private readonly _debugNameData: DebugNameData, + public readonly _debugNameData: DebugNameData, public readonly _computeFn: (reader: IReader, changeSummary: TChangeSummary) => T, private readonly createChangeSummary: (() => TChangeSummary) | undefined, private readonly _handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined, diff --git a/src/vs/base/common/observableInternal/lazyObservableValue.ts b/src/vs/base/common/observableInternal/lazyObservableValue.ts new file mode 100644 index 00000000000..1c35f458161 --- /dev/null +++ b/src/vs/base/common/observableInternal/lazyObservableValue.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EqualityComparer } from 'vs/base/common/equals'; +import { ISettableObservable, ITransaction } from 'vs/base/common/observable'; +import { BaseObservable, IObserver, TransactionImpl } from 'vs/base/common/observableInternal/base'; +import { DebugNameData } from 'vs/base/common/observableInternal/debugName'; + +/** + * Holds off updating observers until the value is actually read. +*/ +export class LazyObservableValue + extends BaseObservable + implements ISettableObservable { + protected _value: T; + private _isUpToDate = true; + private readonly _deltas: TChange[] = []; + + get debugName() { + return this._debugNameData.getDebugName(this) ?? 'LazyObservableValue'; + } + + constructor( + private readonly _debugNameData: DebugNameData, + initialValue: T, + private readonly _equalityComparator: EqualityComparer, + ) { + super(); + this._value = initialValue; + } + + public override get(): T { + this._update(); + return this._value; + } + + private _update(): void { + if (this._isUpToDate) { + return; + } + this._isUpToDate = true; + + if (this._deltas.length > 0) { + for (const observer of this.observers) { + for (const change of this._deltas) { + observer.handleChange(this, change); + } + } + this._deltas.length = 0; + } else { + for (const observer of this.observers) { + observer.handleChange(this, undefined); + } + } + } + + private _updateCounter = 0; + + private _beginUpdate(): void { + this._updateCounter++; + if (this._updateCounter === 1) { + for (const observer of this.observers) { + observer.beginUpdate(this); + } + } + } + + private _endUpdate(): void { + this._updateCounter--; + if (this._updateCounter === 0) { + this._update(); + + // End update could change the observer list. + const observers = [...this.observers]; + for (const r of observers) { + r.endUpdate(this); + } + } + } + + public override addObserver(observer: IObserver): void { + const shouldCallBeginUpdate = !this.observers.has(observer) && this._updateCounter > 0; + super.addObserver(observer); + + if (shouldCallBeginUpdate) { + observer.beginUpdate(this); + } + } + + public override removeObserver(observer: IObserver): void { + const shouldCallEndUpdate = this.observers.has(observer) && this._updateCounter > 0; + super.removeObserver(observer); + + if (shouldCallEndUpdate) { + // Calling end update after removing the observer makes sure endUpdate cannot be called twice here. + observer.endUpdate(this); + } + } + + public set(value: T, tx: ITransaction | undefined, change: TChange): void { + if (change === undefined && this._equalityComparator(this._value, value)) { + return; + } + + let _tx: TransactionImpl | undefined; + if (!tx) { + tx = _tx = new TransactionImpl(() => { }, () => `Setting ${this.debugName}`); + } + try { + this._isUpToDate = false; + this._setValue(value); + if (change !== undefined) { + this._deltas.push(change); + } + + tx.updateObserver({ + beginUpdate: () => this._beginUpdate(), + endUpdate: () => this._endUpdate(), + handleChange: (observable, change) => { }, + handlePossibleChange: (observable) => { }, + }, this); + + if (this._updateCounter > 1) { + // We already started begin/end update, so we need to manually call handlePossibleChange + for (const observer of this.observers) { + observer.handlePossibleChange(this); + } + } + + } finally { + if (_tx) { + _tx.finish(); + } + } + } + + override toString(): string { + return `${this.debugName}: ${this._value}`; + } + + protected _setValue(newValue: T): void { + this._value = newValue; + } +} diff --git a/src/vs/base/common/observableInternal/logging.ts b/src/vs/base/common/observableInternal/logging.ts index 01fcc3cdbbf..5e4712e6923 100644 --- a/src/vs/base/common/observableInternal/logging.ts +++ b/src/vs/base/common/observableInternal/logging.ts @@ -114,7 +114,7 @@ export class ConsoleObservableLogger implements IObservableLogger { styled(derived.debugName, { color: 'BlueViolet' }), ...this.formatInfo(info), this.formatChanges(changedObservables), - { data: [{ fn: derived._computeFn }] } + { data: [{ fn: derived._debugNameData.referenceFn ?? derived._computeFn }] } ])); changedObservables.clear(); } @@ -143,7 +143,7 @@ export class ConsoleObservableLogger implements IObservableLogger { formatKind('autorun'), styled(autorun.debugName, { color: 'BlueViolet' }), this.formatChanges(changedObservables), - { data: [{ fn: autorun._runFn }] } + { data: [{ fn: autorun._debugNameData.referenceFn ?? autorun._runFn }] } ])); changedObservables.clear(); this.indentation++; diff --git a/src/vs/base/common/observableInternal/promise.ts b/src/vs/base/common/observableInternal/promise.ts index e0109a3941b..80d269c16bd 100644 --- a/src/vs/base/common/observableInternal/promise.ts +++ b/src/vs/base/common/observableInternal/promise.ts @@ -6,7 +6,7 @@ import { autorun } from 'vs/base/common/observableInternal/autorun'; import { IObservable, IReader, observableValue, transaction } from './base'; import { Derived, derived } from 'vs/base/common/observableInternal/derived'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { DebugNameData, Owner } from 'vs/base/common/observableInternal/debugName'; +import { DebugNameData, DebugOwner } from 'vs/base/common/observableInternal/debugName'; import { strictEquals } from 'vs/base/common/equals'; import { CancellationError } from 'vs/base/common/errors'; @@ -40,6 +40,10 @@ export class ObservableLazy { * A promise whose state is observable. */ export class ObservablePromise { + public static fromFn(fn: () => Promise): ObservablePromise { + return new ObservablePromise(fn()); + } + private readonly _value = observableValue | undefined>(this, undefined); /** @@ -179,7 +183,7 @@ export function derivedWithCancellationToken(computeFn: (reader: IReader, can export function derivedWithCancellationToken(owner: object, computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable; export function derivedWithCancellationToken(computeFnOrOwner: ((reader: IReader, cancellationToken: CancellationToken) => T) | object, computeFnOrUndefined?: ((reader: IReader, cancellationToken: CancellationToken) => T)): IObservable { let computeFn: (reader: IReader, store: CancellationToken) => T; - let owner: Owner; + let owner: DebugOwner; if (computeFnOrUndefined === undefined) { computeFn = computeFnOrOwner as any; owner = undefined; diff --git a/src/vs/base/common/observableInternal/utils.ts b/src/vs/base/common/observableInternal/utils.ts index 409fe4a10ec..1d012de3d54 100644 --- a/src/vs/base/common/observableInternal/utils.ts +++ b/src/vs/base/common/observableInternal/utils.ts @@ -5,12 +5,14 @@ import { Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { autorun } from 'vs/base/common/observableInternal/autorun'; +import { autorun, autorunOpts } from 'vs/base/common/observableInternal/autorun'; import { BaseObservable, ConvenientObservable, IObservable, IObserver, IReader, ITransaction, _setKeepObserved, _setRecomputeInitiallyAndOnChange, observableValue, subtransaction, transaction } from 'vs/base/common/observableInternal/base'; -import { DebugNameData, Owner, getFunctionName } from 'vs/base/common/observableInternal/debugName'; +import { DebugNameData, IDebugNameData, DebugOwner, getDebugName, } from 'vs/base/common/observableInternal/debugName'; import { derived, derivedOpts } from 'vs/base/common/observableInternal/derived'; import { getLogger } from 'vs/base/common/observableInternal/logging'; import { IValueWithChangeEvent } from '../event'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { EqualityComparer, strictEquals } from 'vs/base/common/equals'; /** * Represents an efficient observable whose value never changes. @@ -52,11 +54,49 @@ export function observableFromPromise(promise: Promise): IObservable<{ val return observable; } + +export function observableFromEvent( + owner: DebugOwner, + event: Event, + getValue: (args: TArgs | undefined) => T, +): IObservable; export function observableFromEvent( event: Event, - getValue: (args: TArgs | undefined) => T + getValue: (args: TArgs | undefined) => T, +): IObservable; +export function observableFromEvent(...args: + [owner: DebugOwner, event: Event, getValue: (args: any | undefined) => any] + | [event: Event, getValue: (args: any | undefined) => any] +): IObservable { + let owner; + let event; + let getValue; + if (args.length === 3) { + [owner, event, getValue] = args; + } else { + [event, getValue] = args; + } + return new FromEventObservable( + new DebugNameData(owner, undefined, getValue), + event, + getValue, + () => FromEventObservable.globalTransaction, + strictEquals + ); +} + +export function observableFromEventOpts( + options: IDebugNameData & { + equalsFn?: EqualityComparer; + }, + event: Event, + getValue: (args: TArgs | undefined) => T, ): IObservable { - return new FromEventObservable(event, getValue); + return new FromEventObservable( + new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? getValue), + event, + getValue, () => FromEventObservable.globalTransaction, options.equalsFn ?? strictEquals + ); } export class FromEventObservable extends BaseObservable { @@ -67,14 +107,17 @@ export class FromEventObservable extends BaseObservable { private subscription: IDisposable | undefined; constructor( + private readonly _debugNameData: DebugNameData, private readonly event: Event, - public readonly _getValue: (args: TArgs | undefined) => T + public readonly _getValue: (args: TArgs | undefined) => T, + private readonly _getTransaction: () => ITransaction | undefined, + private readonly _equalityComparator: EqualityComparer ) { super(); } private getDebugName(): string | undefined { - return getFunctionName(this._getValue); + return this._debugNameData.getDebugName(this); } public get debugName(): string { @@ -90,7 +133,7 @@ export class FromEventObservable extends BaseObservable { const newValue = this._getValue(args); const oldValue = this.value; - const didChange = !this.hasValue || oldValue !== newValue; + const didChange = !this.hasValue || !(this._equalityComparator(oldValue!, newValue)); let didRunTransaction = false; if (didChange) { @@ -99,7 +142,7 @@ export class FromEventObservable extends BaseObservable { if (this.hasValue) { didRunTransaction = true; subtransaction( - FromEventObservable.globalTransaction, + this._getTransaction(), (tx) => { getLogger()?.handleFromEventObservableTriggered(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue }); @@ -229,6 +272,10 @@ class ObservableSignal extends BaseObservable implements return new DebugNameData(this._owner, this._debugName, undefined).getDebugName(this) ?? 'Observable Signal'; } + public override toString(): string { + return this.debugName; + } + constructor( private readonly _debugName: string | undefined, private readonly _owner?: object, @@ -406,9 +453,9 @@ export class KeepAliveObserver implements IObserver { } } -export function derivedObservableWithCache(owner: Owner, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable { +export function derivedObservableWithCache(owner: DebugOwner, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable { let lastValue: T | undefined = undefined; - const observable = derived(owner, reader => { + const observable = derivedOpts({ owner, debugReferenceFn: computeFn }, reader => { lastValue = computeFn(reader, lastValue); return lastValue; }); @@ -439,7 +486,7 @@ export function derivedObservableWithWritableCache(owner: object, computeFn: /** * When the items array changes, referential equal items are not mapped again. */ -export function mapObservableArrayCached(owner: Owner, items: IObservable, map: (input: TIn, store: DisposableStore) => TOut, keySelector?: (input: TIn) => TKey): IObservable { +export function mapObservableArrayCached(owner: DebugOwner, items: IObservable, map: (input: TIn, store: DisposableStore) => TOut, keySelector?: (input: TIn) => TKey): IObservable { let m = new ArrayMap(map, keySelector); const self = derivedOpts({ debugReferenceFn: map, @@ -515,9 +562,49 @@ export class ValueWithChangeEventFromObservable implements IValueWithChangeEv } } -export function observableFromValueWithChangeEvent(_owner: Owner, value: IValueWithChangeEvent): IObservable { +export function observableFromValueWithChangeEvent(owner: DebugOwner, value: IValueWithChangeEvent): IObservable { if (value instanceof ValueWithChangeEventFromObservable) { return value.observable; } - return observableFromEvent(value.onDidChange, () => value.value); + return observableFromEvent(owner, value.onDidChange, () => value.value); +} + +/** + * Creates an observable that has the latest changed value of the given observables. + * Initially (and when not observed), it has the value of the last observable. + * When observed and any of the observables change, it has the value of the last changed observable. + * If multiple observables change in the same transaction, the last observable wins. +*/ +export function latestChangedValue[]>(owner: DebugOwner, observables: T): IObservable> { + if (observables.length === 0) { + throw new BugIndicatingError(); + } + + let hasLastChangedValue = false; + let lastChangedValue: any = undefined; + + const result = observableFromEvent(owner, cb => { + const store = new DisposableStore(); + for (const o of observables) { + store.add(autorunOpts({ debugName: () => getDebugName(result, new DebugNameData(owner, undefined, undefined)) + '.updateLastChangedValue' }, reader => { + hasLastChangedValue = true; + lastChangedValue = o.read(reader); + cb(); + })); + } + store.add({ + dispose() { + hasLastChangedValue = false; + lastChangedValue = undefined; + }, + }); + return store; + }, () => { + if (hasLastChangedValue) { + return lastChangedValue; + } else { + return observables[observables.length - 1].get(); + } + }); + return result; } diff --git a/src/vs/base/common/performance.js b/src/vs/base/common/performance.js index aff4d0734de..2af54743f33 100644 --- a/src/vs/base/common/performance.js +++ b/src/vs/base/common/performance.js @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - //@ts-check +'use strict'; (function () { @@ -42,6 +41,7 @@ // Identify browser environment when following property is not present // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#performancenodetiming + // @ts-ignore if (typeof performance === 'object' && typeof performance.mark === 'function' && !performance.nodeTiming) { // in a browser context, reuse performance-util @@ -119,6 +119,7 @@ module.exports = _factory(sharedObj); } else { console.trace('perf-util defined in UNKNOWN context (neither requirejs or commonjs)'); + // @ts-ignore sharedObj.perf = _factory(sharedObj); } diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index c8ebe38f8c7..28114286e3d 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -189,6 +189,7 @@ export interface IProductConfiguration { readonly extensionPointExtensionKind?: { readonly [extensionPointId: string]: ('ui' | 'workspace' | 'web')[] }; readonly extensionSyncedKeys?: { readonly [extensionId: string]: string[] }; + readonly extensionsEnabledWithApiProposalVersion?: string[]; readonly extensionEnabledApiProposals?: { readonly [extensionId: string]: string[] }; readonly extensionUntrustedWorkspaceSupport?: { readonly [extensionId: string]: ExtensionUntrustedWorkspaceSupport }; readonly extensionVirtualWorkspacesSupport?: { readonly [extensionId: string]: ExtensionVirtualWorkspaceSupport }; diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 025df98b423..1c0ce081625 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -60,14 +60,6 @@ async function rimraf(path: string, mode = RimRafMode.UNLINK, moveToPath?: strin async function rimrafMove(path: string, moveToPath = randomPath(tmpdir())): Promise { try { try { - // Intentionally using `fs.promises` here to skip - // the patched graceful-fs method that can result - // in very long running `rename` calls when the - // folder is locked by a file watcher. We do not - // really want to slow down this operation more - // than necessary and we have a fallback to delete - // via unlink. - // https://github.com/microsoft/vscode/issues/139908 await fs.promises.rename(path, moveToPath); } catch (error) { if (error.code === 'ENOENT') { @@ -87,7 +79,7 @@ async function rimrafMove(path: string, moveToPath = randomPath(tmpdir())): Prom } async function rimrafUnlink(path: string): Promise { - return promisify(fs.rm)(path, { recursive: true, force: true, maxRetries: 3 }); + return fs.promises.rm(path, { recursive: true, force: true, maxRetries: 3 }); } export function rimrafSync(path: string): void { @@ -118,12 +110,12 @@ export interface IDirent { async function readdir(path: string): Promise; async function readdir(path: string, options: { withFileTypes: true }): Promise; async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> { - return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : promisify(fs.readdir)(path))); + return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : fs.promises.readdir(path))); } async function safeReaddirWithFileTypes(path: string): Promise { try { - return await promisify(fs.readdir)(path, { withFileTypes: true }); + return await fs.promises.readdir(path, { withFileTypes: true }); } catch (error) { console.warn('[node.js fs] readdir with filetypes failed with error: ', error); } @@ -493,7 +485,7 @@ function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptio * - allows to move across multiple disks * - attempts to retry the operation for certain error codes on Windows */ -async function rename(source: string, target: string, windowsRetryTimeout: number | false = 60000 /* matches graceful-fs */): Promise { +async function rename(source: string, target: string, windowsRetryTimeout: number | false = 60000): Promise { if (source === target) { return; // simulate node.js behaviour here and do a no-op if paths match } @@ -501,12 +493,10 @@ async function rename(source: string, target: string, windowsRetryTimeout: numbe try { if (isWindows && typeof windowsRetryTimeout === 'number') { // On Windows, a rename can fail when either source or target - // is locked by AV software. We do leverage graceful-fs to iron - // out these issues, however in case the target file exists, - // graceful-fs will immediately return without retry for fs.rename(). + // is locked by AV software. await renameWithRetry(source, target, Date.now(), windowsRetryTimeout); } else { - await promisify(fs.rename)(source, target); + await fs.promises.rename(source, target); } } catch (error) { // In two cases we fallback to classic copy and delete: @@ -528,7 +518,7 @@ async function rename(source: string, target: string, windowsRetryTimeout: numbe async function renameWithRetry(source: string, target: string, startTime: number, retryTimeout: number, attempt = 0): Promise { try { - return await promisify(fs.rename)(source, target); + return await fs.promises.rename(source, target); } catch (error) { if (error.code !== 'EACCES' && error.code !== 'EPERM' && error.code !== 'EBUSY') { throw error; // only for errors we think are temporary @@ -670,30 +660,27 @@ async function doCopySymlink(source: string, target: string, payload: ICopyPaylo //#region Promise based fs methods /** - * Prefer this helper class over the `fs.promises` API to - * enable `graceful-fs` to function properly. Given issue - * https://github.com/isaacs/node-graceful-fs/issues/160 it - * is evident that the module only takes care of the non-promise - * based fs methods. + * Provides promise based 'fs' methods by wrapping around the + * original callback based methods. * - * Another reason is `realpath` being entirely different in - * the promise based implementation compared to the other - * one (https://github.com/microsoft/vscode/issues/118562) + * At least `realpath` is implemented differently in the promise + * based implementation compared to the callback based one. The + * promise based implementation actually calls `fs.realpath.native`. + * (https://github.com/microsoft/vscode/issues/118562) * - * Note: using getters for a reason, since `graceful-fs` - * patching might kick in later after modules have been - * loaded we need to defer access to fs methods. - * (https://github.com/microsoft/vscode/issues/124176) + * TODO@bpasero we should move away from this towards `fs.promises` + * eventually and only keep those methods around where we explicitly + * want the callback based behaviour. */ export const Promises = new class { //#region Implemented by node.js - get access() { return promisify(fs.access); } + get access() { return fs.promises.access; } - get stat() { return promisify(fs.stat); } - get lstat() { return promisify(fs.lstat); } - get utimes() { return promisify(fs.utimes); } + get stat() { return fs.promises.stat; } + get lstat() { return fs.promises.lstat; } + get utimes() { return fs.promises.utimes; } get read() { @@ -713,7 +700,7 @@ export const Promises = new class { }); }; } - get readFile() { return promisify(fs.readFile); } + get readFile() { return fs.promises.readFile; } get write() { @@ -734,27 +721,27 @@ export const Promises = new class { }; } - get appendFile() { return promisify(fs.appendFile); } + get appendFile() { return fs.promises.appendFile; } - get fdatasync() { return promisify(fs.fdatasync); } - get truncate() { return promisify(fs.truncate); } + get fdatasync() { return promisify(fs.fdatasync); } // not exposed as API in 20.x yet + get truncate() { return fs.promises.truncate; } - get copyFile() { return promisify(fs.copyFile); } + get copyFile() { return fs.promises.copyFile; } - get open() { return promisify(fs.open); } - get close() { return promisify(fs.close); } + get open() { return promisify(fs.open); } // changed to return `FileHandle` in promise API + get close() { return promisify(fs.close); } // not exposed as API due to the `FileHandle` return type of `open` - get symlink() { return promisify(fs.symlink); } - get readlink() { return promisify(fs.readlink); } + get symlink() { return fs.promises.symlink; } + get readlink() { return fs.promises.readlink; } - get chmod() { return promisify(fs.chmod); } + get chmod() { return fs.promises.chmod; } - get mkdir() { return promisify(fs.mkdir); } + get mkdir() { return fs.promises.mkdir; } - get unlink() { return promisify(fs.unlink); } - get rmdir() { return promisify(fs.rmdir); } + get unlink() { return fs.promises.unlink; } + get rmdir() { return fs.promises.rmdir; } - get realpath() { return promisify(fs.realpath); } + get realpath() { return promisify(fs.realpath); } // `fs.promises.realpath` will use `fs.realpath.native` which we do not want //#endregion diff --git a/src/vs/base/node/unc.js b/src/vs/base/node/unc.js index b0af4d38b68..e019e5258ce 100644 --- a/src/vs/base/node/unc.js +++ b/src/vs/base/node/unc.js @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; +/// //@ts-check +'use strict'; (function () { function factory() { @@ -18,6 +19,7 @@ // The property `process.uncHostAllowlist` is not available in official node.js // releases, only in our own builds, so we have to probe for availability + // @ts-ignore return process.uncHostAllowlist; } @@ -114,6 +116,7 @@ return; } + // @ts-ignore process.restrictUNCAccess = false; } @@ -122,6 +125,7 @@ return true; } + // @ts-ignore return process.restrictUNCAccess === false; } diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 6530fac0d7b..f1bed382c62 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -426,18 +426,18 @@ export class ChannelServer implements IChannelServer { - this.sendResponse({ id, data, type: ResponseType.PromiseSuccess }); + this.sendResponse({ id, data, type: ResponseType.PromiseSuccess }); }, err => { if (err instanceof Error) { - this.sendResponse({ + this.sendResponse({ id, data: { message: err.message, name: err.name, - stack: err.stack ? (err.stack.split ? err.stack.split('\n') : err.stack) : undefined + stack: err.stack ? err.stack.split('\n') : undefined }, type: ResponseType.PromiseError }); } else { - this.sendResponse({ id, data: err, type: ResponseType.PromiseErrorObj }); + this.sendResponse({ id, data: err, type: ResponseType.PromiseErrorObj }); } }).finally(() => { disposable.dispose(); @@ -458,7 +458,7 @@ export class ChannelServer implements IChannelServer this.sendResponse({ id, data, type: ResponseType.EventFire })); + const disposable = event(data => this.sendResponse({ id, data, type: ResponseType.EventFire })); this.activeRequests.set(request.id, disposable); } @@ -484,7 +484,7 @@ export class ChannelServer implements IChannelServer{ + this.sendResponse({ id: request.id, data: { name: 'Unknown channel', message: `Channel name '${request.channelName}' timed out after ${this.timeoutDelay}ms`, stack: undefined }, type: ResponseType.PromiseError diff --git a/src/vs/base/parts/ipc/node/ipc.net.ts b/src/vs/base/parts/ipc/node/ipc.net.ts index 7d57ca6cb47..0f3dd812b6d 100644 --- a/src/vs/base/parts/ipc/node/ipc.net.ts +++ b/src/vs/base/parts/ipc/node/ipc.net.ts @@ -17,6 +17,15 @@ import { generateUuid } from 'vs/base/common/uuid'; import { ClientConnectionEvent, IPCServer } from 'vs/base/parts/ipc/common/ipc'; import { ChunkStream, Client, ISocket, Protocol, SocketCloseEvent, SocketCloseEventType, SocketDiagnostics, SocketDiagnosticsEventType } from 'vs/base/parts/ipc/common/ipc.net'; +/** + * Maximum time to wait for a 'close' event to fire after the socket stream + * ends. For unix domain sockets, the close event may not fire consistently + * due to what appears to be a Node.js bug. + * + * @see https://github.com/microsoft/vscode/issues/211462#issuecomment-2155471996 + */ +const socketEndTimeoutMs = 30_000; + export class NodeSocket implements ISocket { public readonly debugLabel: string; @@ -51,15 +60,20 @@ export class NodeSocket implements ISocket { }; this.socket.on('error', this._errorListener); + let endTimeoutHandle: NodeJS.Timeout | undefined; this._closeListener = (hadError: boolean) => { this.traceSocketEvent(SocketDiagnosticsEventType.Close, { hadError }); this._canWrite = false; + if (endTimeoutHandle) { + clearTimeout(endTimeoutHandle); + } }; this.socket.on('close', this._closeListener); this._endListener = () => { this.traceSocketEvent(SocketDiagnosticsEventType.NodeEndReceived); this._canWrite = false; + endTimeoutHandle = setTimeout(() => socket.destroy(), socketEndTimeoutMs); }; this.socket.on('end', this._endListener); } diff --git a/src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts b/src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts index 00813be686f..c49d9bf8855 100644 --- a/src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts +++ b/src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { Client as MessagePortClient } from 'vs/base/parts/ipc/browser/ipc.mp'; diff --git a/src/vs/base/parts/ipc/test/common/ipc.test.ts b/src/vs/base/parts/ipc/test/common/ipc.test.ts index 0515d24a49c..b9fc881a222 100644 --- a/src/vs/base/parts/ipc/test/common/ipc.test.ts +++ b/src/vs/base/parts/ipc/test/common/ipc.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; diff --git a/src/vs/base/parts/ipc/test/electron-sandbox/ipc.mp.test.ts b/src/vs/base/parts/ipc/test/electron-sandbox/ipc.mp.test.ts index 44559562cea..4bb851ebffe 100644 --- a/src/vs/base/parts/ipc/test/electron-sandbox/ipc.mp.test.ts +++ b/src/vs/base/parts/ipc/test/electron-sandbox/ipc.mp.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Client as MessagePortClient } from 'vs/base/parts/ipc/browser/ipc.mp'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/parts/ipc/test/node/ipc.cp.integrationTest.ts b/src/vs/base/parts/ipc/test/node/ipc.cp.integrationTest.ts index d761f720de7..9caacd5b537 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.cp.integrationTest.ts +++ b/src/vs/base/parts/ipc/test/node/ipc.cp.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; diff --git a/src/vs/base/parts/ipc/test/node/ipc.net.test.ts b/src/vs/base/parts/ipc/test/node/ipc.net.test.ts index 0e018300cea..bac8dc992f6 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.net.test.ts +++ b/src/vs/base/parts/ipc/test/node/ipc.net.test.ts @@ -3,14 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; +import sinon from 'sinon'; import { EventEmitter } from 'events'; import { AddressInfo, connect, createServer, Server, Socket } from 'net'; import { tmpdir } from 'os'; import { Barrier, timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { ILoadEstimator, PersistentProtocol, Protocol, ProtocolConstants, SocketCloseEvent, SocketDiagnosticsEventType } from 'vs/base/parts/ipc/common/ipc.net'; import { createRandomIPCHandle, createStaticIPCHandle, NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { flakySuite } from 'vs/base/test/common/testUtils'; @@ -134,7 +135,7 @@ class Ether { suite('IPC, Socket Protocol', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + const ds = ensureNoDisposablesAreLeakedInTestSuite(); let ether: Ether; @@ -186,6 +187,26 @@ suite('IPC, Socket Protocol', () => { b.dispose(); }); + + + test('issue #211462: destroy socket after end timeout', async () => { + const socket = new EventEmitter(); + Object.assign(socket, { destroy: () => socket.emit('close') }); + const protocol = ds.add(new Protocol(new NodeSocket(socket as Socket))); + + const disposed = sinon.stub(); + const timers = sinon.useFakeTimers(); + + ds.add(toDisposable(() => timers.restore())); + ds.add(protocol.onDidDispose(disposed)); + + socket.emit('end'); + assert.ok(!disposed.called); + timers.tick(29_999); + assert.ok(!disposed.called); + timers.tick(1); + assert.ok(disposed.called); + }); }); suite('PersistentProtocol reconnection', () => { diff --git a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts index ba8ea6446a6..a132d7d6eb4 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts @@ -172,3 +172,19 @@ export interface AuthInfo { port: number; realm: string; } + +export interface WebUtils { + + // Docs: https://electronjs.org/docs/api/web-utils + + /** + * The file system path that this `File` object points to. In the case where the + * object passed in is not a `File` object an exception is thrown. In the case + * where the File object passed in was constructed in JS and is not backed by a + * file on disk an empty string is returned. + * + * This method superceded the previous augmentation to the `File` object with the + * `path` property. An example is included below. + */ + getPathForFile(file: File): string; +} diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index 44a54904e26..10737a23101 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts @@ -5,7 +5,7 @@ import { INodeProcess, IProcessEnvironment } from 'vs/base/common/platform'; import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes'; -import { IpcRenderer, ProcessMemoryInfo, WebFrame } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; +import { IpcRenderer, ProcessMemoryInfo, WebFrame, WebUtils } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; /** * In Electron renderers we cannot expose all of the `process` global of node.js @@ -121,6 +121,7 @@ export const ipcMessagePort: IpcMessagePort = vscodeGlobal.ipcMessagePort; export const webFrame: WebFrame = vscodeGlobal.webFrame; export const process: ISandboxNodeProcess = vscodeGlobal.process; export const context: ISandboxContext = vscodeGlobal.context; +export const webUtils: WebUtils = vscodeGlobal.webUtils; /** * A set of globals that are available in all windows that either diff --git a/src/vs/base/parts/sandbox/electron-sandbox/preload.js b/src/vs/base/parts/sandbox/electron-sandbox/preload.js index 90ac940861f..7e2339e49da 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/preload.js +++ b/src/vs/base/parts/sandbox/electron-sandbox/preload.js @@ -7,7 +7,14 @@ (function () { 'use strict'; - const { ipcRenderer, webFrame, contextBridge } = require('electron'); + /** + * @import { ISandboxConfiguration } from '../common/sandboxTypes' + * @import { IpcRenderer } from './electronTypes' + * @import { IpcRendererEvent } from 'electron' + * @import { ISandboxNodeProcess } from './globals' + */ + + const { ipcRenderer, webFrame, contextBridge, webUtils } = require('electron'); //#region Utilities @@ -41,10 +48,6 @@ //#region Resolve Configuration - /** - * @typedef {import('../common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration - */ - /** @type {ISandboxConfiguration | undefined} */ let configuration = undefined; @@ -123,9 +126,6 @@ * A minimal set of methods exposed from Electron's `ipcRenderer` * to support communication to main process. * - * @typedef {import('./electronTypes').IpcRenderer} IpcRenderer - * @typedef {import('electron').IpcRendererEvent} IpcRendererEvent - * * @type {IpcRenderer} */ @@ -237,14 +237,25 @@ } }, + /** + * Support for subset of Electron's `webUtils` type. + */ + webUtils: { + + /** + * @param {File} file + */ + getPathForFile(file) { + return webUtils.getPathForFile(file); + } + }, + /** * Support for a subset of access to node.js global `process`. * * Note: when `sandbox` is enabled, the only properties available * are https://github.com/electron/electron/blob/master/docs/api/process.md#sandbox * - * @typedef {import('./globals').ISandboxNodeProcess} ISandboxNodeProcess - * * @type {ISandboxNodeProcess} */ process: { diff --git a/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts b/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts index 491f2209c6d..7724c4ed465 100644 --- a/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts +++ b/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { context, ipcRenderer, process, webFrame } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import assert from 'assert'; +import { context, ipcRenderer, process, webFrame, webUtils } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Sandbox', () => { @@ -13,6 +13,7 @@ suite('Sandbox', () => { assert.ok(typeof ipcRenderer.send === 'function'); assert.ok(typeof webFrame.setZoomLevel === 'function'); assert.ok(typeof process.platform === 'string'); + assert.ok(typeof webUtils.getPathForFile === 'function'); const config = await context.resolveConfiguration(); assert.ok(config); diff --git a/src/vs/base/test/browser/actionbar.test.ts b/src/vs/base/test/browser/actionbar.test.ts index fc119f36de9..5d197e8181b 100644 --- a/src/vs/base/test/browser/actionbar.test.ts +++ b/src/vs/base/test/browser/actionbar.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ActionBar, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, Separator } from 'vs/base/common/actions'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/browser.test.ts b/src/vs/base/test/browser/browser.test.ts index 115112ff753..76049fb5721 100644 --- a/src/vs/base/test/browser/browser.test.ts +++ b/src/vs/base/test/browser/browser.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/comparers.test.ts b/src/vs/base/test/browser/comparers.test.ts index 8848b04a6e8..690e3eb4782 100644 --- a/src/vs/base/test/browser/comparers.test.ts +++ b/src/vs/base/test/browser/comparers.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { compareFileExtensions, compareFileExtensionsDefault, compareFileExtensionsLower, compareFileExtensionsUnicode, compareFileExtensionsUpper, compareFileNames, compareFileNamesDefault, compareFileNamesLower, compareFileNamesUnicode, compareFileNamesUpper } from 'vs/base/common/comparers'; diff --git a/src/vs/base/test/browser/dom.test.ts b/src/vs/base/test/browser/dom.test.ts index 03d618b1f0c..98ad9441a92 100644 --- a/src/vs/base/test/browser/dom.test.ts +++ b/src/vs/base/test/browser/dom.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { $, asCssValueWithDefault, h, multibyteAwareBtoa, trackAttributes, copyAttributes, disposableWindowInterval, getWindows, getWindowsCount, getWindowId, getWindowById, hasWindow, getWindow, getDocument, isHTMLElement } from 'vs/base/browser/dom'; import { ensureCodeWindow, isAuxiliaryWindow, mainWindow } from 'vs/base/browser/window'; import { DeferredPromise, timeout } from 'vs/base/common/async'; diff --git a/src/vs/base/test/browser/formattedTextRenderer.test.ts b/src/vs/base/test/browser/formattedTextRenderer.test.ts index 12acef6e7b8..d4ba452786d 100644 --- a/src/vs/base/test/browser/formattedTextRenderer.test.ts +++ b/src/vs/base/test/browser/formattedTextRenderer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { renderFormattedText, renderText } from 'vs/base/browser/formattedTextRenderer'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/hash.test.ts b/src/vs/base/test/browser/hash.test.ts index e613a1913f1..d7f319bbe15 100644 --- a/src/vs/base/test/browser/hash.test.ts +++ b/src/vs/base/test/browser/hash.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { sha1Hex } from 'vs/base/browser/hash'; import { hash, StringSHA1 } from 'vs/base/common/hash'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/highlightedLabel.test.ts b/src/vs/base/test/browser/highlightedLabel.test.ts index fe2ceb43d61..c18b2037062 100644 --- a/src/vs/base/test/browser/highlightedLabel.test.ts +++ b/src/vs/base/test/browser/highlightedLabel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/iconLabels.test.ts b/src/vs/base/test/browser/iconLabels.test.ts index 3972977f65f..2a10f47241b 100644 --- a/src/vs/base/test/browser/iconLabels.test.ts +++ b/src/vs/base/test/browser/iconLabels.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isHTMLElement } from 'vs/base/browser/dom'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/indexedDB.test.ts b/src/vs/base/test/browser/indexedDB.test.ts index a6266231292..8b1271562fd 100644 --- a/src/vs/base/test/browser/indexedDB.test.ts +++ b/src/vs/base/test/browser/indexedDB.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IndexedDB } from 'vs/base/browser/indexedDB'; import { flakySuite } from 'vs/base/test/common/testUtils'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts index f9099c653e9..4ea38c815c3 100644 --- a/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { fillInIncompleteTokens, renderMarkdown, renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { marked } from 'vs/base/common/marked/marked'; @@ -607,6 +607,15 @@ const y = 2; assert.deepStrictEqual(newTokens, completeTokens); }); + test(`incomplete ${name} in asterisk list`, () => { + const text = `* list item one\n* list item two and ${delimiter}text`; + const tokens = marked.lexer(text); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.lexer(text + delimiter); + assert.deepStrictEqual(newTokens, completeTokens); + }); + test(`incomplete ${name} in numbered list`, () => { const text = `1. list item one\n2. list item two and ${delimiter}text`; const tokens = marked.lexer(text); diff --git a/src/vs/base/test/browser/progressBar.test.ts b/src/vs/base/test/browser/progressBar.test.ts index 9620ae31948..8490fd17897 100644 --- a/src/vs/base/test/browser/progressBar.test.ts +++ b/src/vs/base/test/browser/progressBar.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { mainWindow } from 'vs/base/browser/window'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -17,7 +17,7 @@ suite('ProgressBar', () => { }); teardown(() => { - mainWindow.document.body.removeChild(fixture); + fixture.remove(); }); test('Progress Bar', function () { diff --git a/src/vs/base/test/browser/ui/contextview/contextview.test.ts b/src/vs/base/test/browser/ui/contextview/contextview.test.ts index c302c358937..45fded13807 100644 --- a/src/vs/base/test/browser/ui/contextview/contextview.test.ts +++ b/src/vs/base/test/browser/ui/contextview/contextview.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { layout, LayoutAnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/ui/grid/grid.test.ts b/src/vs/base/test/browser/ui/grid/grid.test.ts index 781a0a407f0..92296d1880b 100644 --- a/src/vs/base/test/browser/ui/grid/grid.test.ts +++ b/src/vs/base/test/browser/ui/grid/grid.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { createSerializedGrid, Direction, getRelativeLocation, Grid, GridNode, GridNodeDescriptor, ISerializableView, isGridBranchNode, IViewDeserializer, Orientation, sanitizeGridNodeDescriptor, SerializableGrid, Sizing } from 'vs/base/browser/ui/grid/grid'; import { Event } from 'vs/base/common/event'; import { deepClone } from 'vs/base/common/objects'; diff --git a/src/vs/base/test/browser/ui/grid/gridview.test.ts b/src/vs/base/test/browser/ui/grid/gridview.test.ts index bbc7de0716f..df0c3838e6b 100644 --- a/src/vs/base/test/browser/ui/grid/gridview.test.ts +++ b/src/vs/base/test/browser/ui/grid/gridview.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { $ } from 'vs/base/browser/dom'; import { GridView, IView, Orientation, Sizing } from 'vs/base/browser/ui/grid/gridview'; import { nodesToArrays, TestView } from 'vs/base/test/browser/ui/grid/util'; diff --git a/src/vs/base/test/browser/ui/grid/util.ts b/src/vs/base/test/browser/ui/grid/util.ts index 9ebe81b3c56..ccadd7866e6 100644 --- a/src/vs/base/test/browser/ui/grid/util.ts +++ b/src/vs/base/test/browser/ui/grid/util.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IView } from 'vs/base/browser/ui/grid/grid'; import { GridNode, isGridBranchNode } from 'vs/base/browser/ui/grid/gridview'; import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/base/test/browser/ui/list/listView.test.ts b/src/vs/base/test/browser/ui/list/listView.test.ts index 7dcab8e7d25..0a6dede20cc 100644 --- a/src/vs/base/test/browser/ui/list/listView.test.ts +++ b/src/vs/base/test/browser/ui/list/listView.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ListView } from 'vs/base/browser/ui/list/listView'; import { range } from 'vs/base/common/arrays'; diff --git a/src/vs/base/test/browser/ui/list/listWidget.test.ts b/src/vs/base/test/browser/ui/list/listWidget.test.ts index 28fd9524341..6d1263dd89c 100644 --- a/src/vs/base/test/browser/ui/list/listWidget.test.ts +++ b/src/vs/base/test/browser/ui/list/listWidget.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { range } from 'vs/base/common/arrays'; diff --git a/src/vs/base/test/browser/ui/list/rangeMap.test.ts b/src/vs/base/test/browser/ui/list/rangeMap.test.ts index 5b3b4a6c65f..0a4625c76b8 100644 --- a/src/vs/base/test/browser/ui/list/rangeMap.test.ts +++ b/src/vs/base/test/browser/ui/list/rangeMap.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { consolidate, groupIntersect, RangeMap } from 'vs/base/browser/ui/list/rangeMap'; import { Range } from 'vs/base/common/range'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/ui/menu/menubar.test.ts b/src/vs/base/test/browser/ui/menu/menubar.test.ts index 49420cc0404..af5e1da2e91 100644 --- a/src/vs/base/test/browser/ui/menu/menubar.test.ts +++ b/src/vs/base/test/browser/ui/menu/menubar.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { $ } from 'vs/base/browser/dom'; import { unthemedMenuStyles } from 'vs/base/browser/ui/menu/menu'; import { MenuBar } from 'vs/base/browser/ui/menu/menubar'; diff --git a/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts b/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts index 30879e441db..cc662ab282f 100644 --- a/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts +++ b/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MouseWheelClassifier } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts b/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts index 0a4a2dd00db..39876bc5d91 100644 --- a/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts +++ b/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index 48ee43fe1b3..7e20426f5a8 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Sash, SashState } from 'vs/base/browser/ui/sash/sash'; import { IView, LayoutPriority, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts index c92a6273336..78b94a9577c 100644 --- a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { AsyncDataTree, CompressibleAsyncDataTree, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; diff --git a/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts index 79de7324a24..9c5777b5fc5 100644 --- a/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { compress, CompressedObjectTreeModel, decompress, ICompressedTreeElement, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; import { IObjectTreeModelSetChildrenOptions } from 'vs/base/browser/ui/tree/objectTreeModel'; diff --git a/src/vs/base/test/browser/ui/tree/dataTree.test.ts b/src/vs/base/test/browser/ui/tree/dataTree.test.ts index fb821d3b662..37c586f00e0 100644 --- a/src/vs/base/test/browser/ui/tree/dataTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/dataTree.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; diff --git a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts index 5657e458849..70277e01c87 100644 --- a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IIndexTreeModelSpliceOptions, IIndexTreeNode, IList, IndexTreeModel } from 'vs/base/browser/ui/tree/indexTreeModel'; import { ITreeElement, ITreeFilter, ITreeNode, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { timeout } from 'vs/base/common/async'; diff --git a/src/vs/base/test/browser/ui/tree/objectTree.test.ts b/src/vs/base/test/browser/ui/tree/objectTree.test.ts index 05594bf561a..eaed09db694 100644 --- a/src/vs/base/test/browser/ui/tree/objectTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTree.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { CompressibleObjectTree, ICompressibleTreeRenderer, ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; diff --git a/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts index 4c895d94130..ffef4afb72b 100644 --- a/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; import { ITreeFilter, ITreeNode, ObjectTreeElementCollapseState, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; diff --git a/src/vs/base/test/common/arrays.test.ts b/src/vs/base/test/common/arrays.test.ts index 25a2620a86a..617a766c965 100644 --- a/src/vs/base/test/common/arrays.test.ts +++ b/src/vs/base/test/common/arrays.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as arrays from 'vs/base/common/arrays'; import * as arraysFind from 'vs/base/common/arraysFind'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/arraysFind.test.ts b/src/vs/base/test/common/arraysFind.test.ts index d932b68cbba..caeea0fed22 100644 --- a/src/vs/base/test/common/arraysFind.test.ts +++ b/src/vs/base/test/common/arraysFind.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MonotonousArray, findFirstMonotonous, findLastMonotonous } from 'vs/base/common/arraysFind'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/assert.test.ts b/src/vs/base/test/common/assert.test.ts index ed4e60e8222..f0052bade97 100644 --- a/src/vs/base/test/common/assert.test.ts +++ b/src/vs/base/test/common/assert.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ok } from 'vs/base/common/assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index da309f944bf..e55200ff192 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as async from 'vs/base/common/async'; import * as MicrotaskDelay from "vs/base/common/symbols"; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; diff --git a/src/vs/base/test/common/buffer.test.ts b/src/vs/base/test/common/buffer.test.ts index 6c869a16b3f..e94fd8755a0 100644 --- a/src/vs/base/test/common/buffer.test.ts +++ b/src/vs/base/test/common/buffer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { bufferedStreamToBuffer, bufferToReadable, bufferToStream, decodeBase64, encodeBase64, newWriteableBufferStream, readableToBuffer, streamToBuffer, VSBuffer } from 'vs/base/common/buffer'; import { peekStream } from 'vs/base/common/stream'; diff --git a/src/vs/base/test/common/cache.test.ts b/src/vs/base/test/common/cache.test.ts index b1946ed354a..ed4ca02f93b 100644 --- a/src/vs/base/test/common/cache.test.ts +++ b/src/vs/base/test/common/cache.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { Cache } from 'vs/base/common/cache'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/cancellation.test.ts b/src/vs/base/test/common/cancellation.test.ts index 2e184c42267..178f77cde1e 100644 --- a/src/vs/base/test/common/cancellation.test.ts +++ b/src/vs/base/test/common/cancellation.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/charCode.test.ts b/src/vs/base/test/common/charCode.test.ts index 49be69d8578..59c4e3fbcc9 100644 --- a/src/vs/base/test/common/charCode.test.ts +++ b/src/vs/base/test/common/charCode.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/collections.test.ts b/src/vs/base/test/common/collections.test.ts index c2616cc377e..b1304b3f30f 100644 --- a/src/vs/base/test/common/collections.test.ts +++ b/src/vs/base/test/common/collections.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as collections from 'vs/base/common/collections'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -32,4 +32,70 @@ suite('Collections', () => { assert.strictEqual(grouped[group2].length, 1); assert.strictEqual(grouped[group2][0].value, value3); }); + + suite('SetWithKey', () => { + let setWithKey: collections.SetWithKey<{ someProp: string }>; + + const initialValues = ['a', 'b', 'c'].map(s => ({ someProp: s })); + setup(() => { + setWithKey = new collections.SetWithKey<{ someProp: string }>(initialValues, value => value.someProp); + }); + + test('size', () => { + assert.strictEqual(setWithKey.size, 3); + }); + + test('add', () => { + setWithKey.add({ someProp: 'd' }); + assert.strictEqual(setWithKey.size, 4); + assert.strictEqual(setWithKey.has({ someProp: 'd' }), true); + }); + + test('delete', () => { + assert.strictEqual(setWithKey.has({ someProp: 'b' }), true); + setWithKey.delete({ someProp: 'b' }); + assert.strictEqual(setWithKey.size, 2); + assert.strictEqual(setWithKey.has({ someProp: 'b' }), false); + }); + + test('has', () => { + assert.strictEqual(setWithKey.has({ someProp: 'a' }), true); + assert.strictEqual(setWithKey.has({ someProp: 'b' }), true); + }); + + test('entries', () => { + const entries = Array.from(setWithKey.entries()); + assert.deepStrictEqual(entries, initialValues.map(value => [value, value])); + }); + + test('keys and values', () => { + const keys = Array.from(setWithKey.keys()); + const values = Array.from(setWithKey.values()); + assert.deepStrictEqual(keys, initialValues); + assert.deepStrictEqual(values, initialValues); + }); + + test('clear', () => { + setWithKey.clear(); + assert.strictEqual(setWithKey.size, 0); + }); + + test('forEach', () => { + const values: any[] = []; + setWithKey.forEach(value => values.push(value)); + assert.deepStrictEqual(values, initialValues); + }); + + test('iterator', () => { + const values: any[] = []; + for (const value of setWithKey) { + values.push(value); + } + assert.deepStrictEqual(values, initialValues); + }); + + test('toStringTag', () => { + assert.strictEqual(setWithKey[Symbol.toStringTag], 'SetWithKey'); + }); + }); }); diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts index 857c394d322..256397a713e 100644 --- a/src/vs/base/test/common/color.test.ts +++ b/src/vs/base/test/common/color.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Color, HSLA, HSVA, RGBA } from 'vs/base/common/color'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/console.test.ts b/src/vs/base/test/common/console.test.ts index 86842d1dcd1..457255eb627 100644 --- a/src/vs/base/test/common/console.test.ts +++ b/src/vs/base/test/common/console.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { getFirstFrame } from 'vs/base/common/console'; import { normalize } from 'vs/base/common/path'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/decorators.test.ts b/src/vs/base/test/common/decorators.test.ts index 92bce8c605b..33d78329dd5 100644 --- a/src/vs/base/test/common/decorators.test.ts +++ b/src/vs/base/test/common/decorators.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { memoize, throttle } from 'vs/base/common/decorators'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/diff/diff.test.ts b/src/vs/base/test/common/diff/diff.test.ts index 353b68ebd7e..eeee6328c83 100644 --- a/src/vs/base/test/common/diff/diff.test.ts +++ b/src/vs/base/test/common/diff/diff.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDiffChange, LcsDiff, StringDiffSequence } from 'vs/base/common/diff/diff'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/errors.test.ts b/src/vs/base/test/common/errors.test.ts index 96210c55314..3ebfaffe4de 100644 --- a/src/vs/base/test/common/errors.test.ts +++ b/src/vs/base/test/common/errors.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index 161c7085fa6..3c65d9e77e9 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { stub } from 'sinon'; import { tail2 } from 'vs/base/common/arrays'; import { DeferredPromise, timeout } from 'vs/base/common/async'; diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index c13210daa13..45e411a7e1c 100644 --- a/src/vs/base/test/common/extpath.test.ts +++ b/src/vs/base/test/common/extpath.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import * as extpath from 'vs/base/common/extpath'; import { isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index 5bfe3856226..5da3552ec56 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { anyScore, createMatches, fuzzyScore, fuzzyScoreGraceful, fuzzyScoreGracefulAggressive, FuzzyScorer, IFilter, IMatch, matchesCamelCase, matchesContiguousSubString, matchesPrefix, matchesStrictPrefix, matchesSubString, matchesWords, or } from 'vs/base/common/filters'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 747e2ee1841..4e41662631d 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { compareItemsByFuzzyScore, FuzzyScore, FuzzyScore2, FuzzyScorerCache, IItemAccessor, IItemScore, pieceToQuery, prepareQuery, scoreFuzzy, scoreFuzzy2, scoreItemFuzzy } from 'vs/base/common/fuzzyScorer'; import { Schemas } from 'vs/base/common/network'; import { basename, dirname, posix, sep, win32 } from 'vs/base/common/path'; diff --git a/src/vs/base/test/common/glob.test.ts b/src/vs/base/test/common/glob.test.ts index 5bfb3dccfb3..2e721bd3002 100644 --- a/src/vs/base/test/common/glob.test.ts +++ b/src/vs/base/test/common/glob.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as glob from 'vs/base/common/glob'; import { sep } from 'vs/base/common/path'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/base/test/common/history.test.ts b/src/vs/base/test/common/history.test.ts index 39143609b88..07443052e78 100644 --- a/src/vs/base/test/common/history.test.ts +++ b/src/vs/base/test/common/history.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { HistoryNavigator, HistoryNavigator2 } from 'vs/base/common/history'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/iconLabels.test.ts b/src/vs/base/test/common/iconLabels.test.ts index 4f2cd802c64..4ce6b493afa 100644 --- a/src/vs/base/test/common/iconLabels.test.ts +++ b/src/vs/base/test/common/iconLabels.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IMatch } from 'vs/base/common/filters'; import { escapeIcons, getCodiconAriaLabel, IParsedLabelWithIcons, markdownEscapeEscapedIcons, matchesFuzzyIconAware, parseLabelWithIcons, stripIcons } from 'vs/base/common/iconLabels'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/iterator.test.ts b/src/vs/base/test/common/iterator.test.ts index ce69814ad43..0d95a4afc24 100644 --- a/src/vs/base/test/common/iterator.test.ts +++ b/src/vs/base/test/common/iterator.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Iterable } from 'vs/base/common/iterator'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/json.test.ts b/src/vs/base/test/common/json.test.ts index 4ad121151b4..c8a6202bf06 100644 --- a/src/vs/base/test/common/json.test.ts +++ b/src/vs/base/test/common/json.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { createScanner, Node, parse, ParseError, ParseErrorCode, ParseOptions, parseTree, ScanError, SyntaxKind } from 'vs/base/common/json'; import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/jsonEdit.test.ts b/src/vs/base/test/common/jsonEdit.test.ts index fa5d41b481a..060884ae9ff 100644 --- a/src/vs/base/test/common/jsonEdit.test.ts +++ b/src/vs/base/test/common/jsonEdit.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { removeProperty, setProperty } from 'vs/base/common/jsonEdit'; import { Edit, FormattingOptions } from 'vs/base/common/jsonFormatter'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/jsonFormatter.test.ts b/src/vs/base/test/common/jsonFormatter.test.ts index 636d987c54a..80d8a22fd35 100644 --- a/src/vs/base/test/common/jsonFormatter.test.ts +++ b/src/vs/base/test/common/jsonFormatter.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as Formatter from 'vs/base/common/jsonFormatter'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/stripComments.test.ts b/src/vs/base/test/common/jsonParse.test.ts similarity index 57% rename from src/vs/base/test/common/stripComments.test.ts rename to src/vs/base/test/common/jsonParse.test.ts index 4aac8cdcb9d..48aa377b2f8 100644 --- a/src/vs/base/test/common/stripComments.test.ts +++ b/src/vs/base/test/common/jsonParse.test.ts @@ -2,14 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; -import { stripComments } from 'vs/base/common/stripComments'; +import { parse, stripComments } from 'vs/base/common/jsonc'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -// We use this regular expression quite often to strip comments in JSON files. - -suite('Strip Comments', () => { +suite('JSON Parse', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('Line comment', () => { @@ -23,7 +21,7 @@ suite('Strip Comments', () => { " \"prop\": 10 ", "}", ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Line comment - EOF', () => { const content: string = [ @@ -36,7 +34,7 @@ suite('Strip Comments', () => { "}", "" ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Line comment - \\r\\n', () => { const content: string = [ @@ -49,7 +47,7 @@ suite('Strip Comments', () => { " \"prop\": 10 ", "}", ].join('\r\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Line comment - EOF - \\r\\n', () => { const content: string = [ @@ -62,7 +60,7 @@ suite('Strip Comments', () => { "}", "" ].join('\r\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Block comment - single line', () => { const content: string = [ @@ -75,7 +73,7 @@ suite('Strip Comments', () => { " \"prop\": 10", "}", ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Block comment - multi line', () => { const content: string = [ @@ -92,7 +90,7 @@ suite('Strip Comments', () => { " \"prop\": 10", "}", ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Block comment - shortest match', () => { const content = "/* abc */ */"; @@ -110,7 +108,7 @@ suite('Strip Comments', () => { " \"/* */\": 10", "}" ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('No strings - single quote', () => { const content: string = [ @@ -136,7 +134,7 @@ suite('Strip Comments', () => { ` "a": 10`, "}" ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Trailing comma in array', () => { const content: string = [ @@ -145,6 +143,52 @@ suite('Strip Comments', () => { const expected: string = [ `[ "a", "b", "c" ]` ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); + }); + + test('Trailing comma', () => { + const content: string = [ + "{", + " \"propA\": 10, // a comment", + " \"propB\": false, // a trailing comma", + "}", + ].join('\n'); + const expected = [ + "{", + " \"propA\": 10,", + " \"propB\": false", + "}", + ].join('\n'); + assert.deepEqual(parse(content), JSON.parse(expected)); + }); + + test('Trailing comma - EOF', () => { + const content = ` +// This configuration file allows you to pass permanent command line arguments to VS Code. +// Only a subset of arguments is currently supported to reduce the likelihood of breaking +// the installation. +// +// PLEASE DO NOT CHANGE WITHOUT UNDERSTANDING THE IMPACT +// +// NOTE: Changing this file requires a restart of VS Code. +{ + // Use software rendering instead of hardware accelerated rendering. + // This can help in cases where you see rendering issues in VS Code. + // "disable-hardware-acceleration": true, + // Allows to disable crash reporting. + // Should restart the app if the value is changed. + "enable-crash-reporter": true, + // Unique id used for correlating crash reports sent from this instance. + // Do not edit this value. + "crash-reporter-id": "aaaaab31-7453-4506-97d0-93411b2c21c7", + "locale": "en", + // "log-level": "trace" +} +`; + assert.deepEqual(parse(content), { + "enable-crash-reporter": true, + "crash-reporter-id": "aaaaab31-7453-4506-97d0-93411b2c21c7", + "locale": "en" + }); }); }); diff --git a/src/vs/base/test/common/keyCodes.test.ts b/src/vs/base/test/common/keyCodes.test.ts index e3c73412eab..ea539be7c62 100644 --- a/src/vs/base/test/common/keyCodes.test.ts +++ b/src/vs/base/test/common/keyCodes.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EVENT_KEY_CODE_MAP, IMMUTABLE_CODE_TO_KEY_CODE, IMMUTABLE_KEY_CODE_TO_CODE, KeyChord, KeyCode, KeyCodeUtils, KeyMod, NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE, ScanCode, ScanCodeUtils } from 'vs/base/common/keyCodes'; import { decodeKeybinding, KeyCodeChord, Keybinding } from 'vs/base/common/keybindings'; import { OperatingSystem } from 'vs/base/common/platform'; diff --git a/src/vs/base/test/common/keybindings.test.ts b/src/vs/base/test/common/keybindings.test.ts index ab26cf24864..5959f510cda 100644 --- a/src/vs/base/test/common/keybindings.test.ts +++ b/src/vs/base/test/common/keybindings.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyCode, ScanCode } from 'vs/base/common/keyCodes'; import { KeyCodeChord, ScanCodeChord } from 'vs/base/common/keybindings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/labels.test.ts b/src/vs/base/test/common/labels.test.ts index 6c9d3eb0f11..b74f0238c16 100644 --- a/src/vs/base/test/common/labels.test.ts +++ b/src/vs/base/test/common/labels.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as labels from 'vs/base/common/labels'; import { isMacintosh, isWindows, OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/base/test/common/lazy.test.ts b/src/vs/base/test/common/lazy.test.ts index 361e3305d43..220d0da6d10 100644 --- a/src/vs/base/test/common/lazy.test.ts +++ b/src/vs/base/test/common/lazy.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Lazy } from 'vs/base/common/lazy'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/lifecycle.test.ts b/src/vs/base/test/common/lifecycle.test.ts index 103009b5219..23080bc8a12 100644 --- a/src/vs/base/test/common/lifecycle.test.ts +++ b/src/vs/base/test/common/lifecycle.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { DisposableStore, dispose, IDisposable, markAsSingleton, ReferenceCollection, SafeDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite, throwIfDisposablesAreLeaked } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/linkedList.test.ts b/src/vs/base/test/common/linkedList.test.ts index 181916d9625..8bd3f6519a7 100644 --- a/src/vs/base/test/common/linkedList.test.ts +++ b/src/vs/base/test/common/linkedList.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { LinkedList } from 'vs/base/common/linkedList'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/linkedText.test.ts b/src/vs/base/test/common/linkedText.test.ts index dc3bdbd0609..63f76d082c4 100644 --- a/src/vs/base/test/common/linkedText.test.ts +++ b/src/vs/base/test/common/linkedText.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { parseLinkedText } from 'vs/base/common/linkedText'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index f92234c3266..f837920f1f9 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { BidirectionalMap, LinkedMap, LRUCache, mapsStrictEqualIgnoreOrder, MRUCache, ResourceMap, SetMap, Touch } from 'vs/base/common/map'; import { extUriIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/base/test/common/markdownString.test.ts b/src/vs/base/test/common/markdownString.test.ts index cb7df1696b3..4c402692512 100644 --- a/src/vs/base/test/common/markdownString.test.ts +++ b/src/vs/base/test/common/markdownString.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/marshalling.test.ts b/src/vs/base/test/common/marshalling.test.ts index 7d3bb66cec7..94d30b5b0bf 100644 --- a/src/vs/base/test/common/marshalling.test.ts +++ b/src/vs/base/test/common/marshalling.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { parse, stringify } from 'vs/base/common/marshalling'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/mime.test.ts b/src/vs/base/test/common/mime.test.ts index fce56646ff8..96fa3b6209e 100644 --- a/src/vs/base/test/common/mime.test.ts +++ b/src/vs/base/test/common/mime.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { normalizeMimeType } from 'vs/base/common/mime'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/network.test.ts b/src/vs/base/test/common/network.test.ts index a85b2b392a8..d741b0e4577 100644 --- a/src/vs/base/test/common/network.test.ts +++ b/src/vs/base/test/common/network.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { isWeb } from 'vs/base/common/platform'; import { isEqual } from 'vs/base/common/resources'; diff --git a/src/vs/base/test/common/normalization.test.ts b/src/vs/base/test/common/normalization.test.ts index 65b44eb652c..58f4fe77e81 100644 --- a/src/vs/base/test/common/normalization.test.ts +++ b/src/vs/base/test/common/normalization.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { removeAccents } from 'vs/base/common/normalization'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/numbers.test.ts b/src/vs/base/test/common/numbers.test.ts new file mode 100644 index 00000000000..7095b7aae40 --- /dev/null +++ b/src/vs/base/test/common/numbers.test.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 * as assert from 'assert'; +import { isPointWithinTriangle } from 'vs/base/common/numbers'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; + +suite('isPointWithinTriangle', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should return true if the point is within the triangle', () => { + const result = isPointWithinTriangle(0.25, 0.25, 0, 0, 1, 0, 0, 1); + assert.ok(result); + }); + + test('should return false if the point is outside the triangle', () => { + const result = isPointWithinTriangle(2, 2, 0, 0, 1, 0, 0, 1); + assert.ok(!result); + }); + + test('should return true if the point is on the edge of the triangle', () => { + const result = isPointWithinTriangle(0.5, 0, 0, 0, 1, 0, 0, 1); + assert.ok(result); + }); +}); diff --git a/src/vs/base/test/common/objects.test.ts b/src/vs/base/test/common/objects.test.ts index 2465585544f..ce600eb0181 100644 --- a/src/vs/base/test/common/objects.test.ts +++ b/src/vs/base/test/common/objects.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as objects from 'vs/base/common/objects'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/observable.test.ts b/src/vs/base/test/common/observable.test.ts index e2694d5c94b..62c7579be12 100644 --- a/src/vs/base/test/common/observable.test.ts +++ b/src/vs/base/test/common/observable.test.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { ISettableObservable, autorun, derived, ITransaction, observableFromEvent, observableValue, transaction, keepObserved, waitForState, autorunHandleChanges, observableSignal } from 'vs/base/common/observable'; import { BaseObservable, IObservable, IObserver } from 'vs/base/common/observableInternal/base'; +import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('observables', () => { @@ -1236,6 +1238,35 @@ suite('observables', () => { 'rejected {\"state\":\"error\"}' ]); }); + + test('derived as lazy', () => { + const store = new DisposableStore(); + const log = new Log(); + let i = 0; + const d = derivedDisposable(() => { + const id = i++; + log.log('myDerived ' + id); + return { + dispose: () => log.log(`disposed ${id}`) + }; + }); + + d.get(); + assert.deepStrictEqual(log.getAndClearEntries(), ['myDerived 0', 'disposed 0']); + d.get(); + assert.deepStrictEqual(log.getAndClearEntries(), ['myDerived 1', 'disposed 1']); + + d.keepObserved(store); + assert.deepStrictEqual(log.getAndClearEntries(), []); + d.get(); + assert.deepStrictEqual(log.getAndClearEntries(), ['myDerived 2']); + d.get(); + assert.deepStrictEqual(log.getAndClearEntries(), []); + + store.dispose(); + + assert.deepStrictEqual(log.getAndClearEntries(), ['disposed 2']); + }); }); test('observableValue', () => { diff --git a/src/vs/base/test/common/paging.test.ts b/src/vs/base/test/common/paging.test.ts index f0ff02a4ab5..772083f3613 100644 --- a/src/vs/base/test/common/paging.test.ts +++ b/src/vs/base/test/common/paging.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { disposableTimeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { CancellationError, isCancellationError } from 'vs/base/common/errors'; diff --git a/src/vs/base/test/common/path.test.ts b/src/vs/base/test/common/path.test.ts index 62b43c91d0b..f42abd1fed3 100644 --- a/src/vs/base/test/common/path.test.ts +++ b/src/vs/base/test/common/path.test.ts @@ -27,7 +27,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -import * as assert from 'assert'; +import assert from 'assert'; import * as path from 'vs/base/common/path'; import { isWeb, isWindows } from 'vs/base/common/platform'; import * as process from 'vs/base/common/process'; diff --git a/src/vs/base/test/common/prefixTree.test.ts b/src/vs/base/test/common/prefixTree.test.ts index e5545734a8a..10b77d96bbc 100644 --- a/src/vs/base/test/common/prefixTree.test.ts +++ b/src/vs/base/test/common/prefixTree.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('WellDefinedPrefixTree', () => { diff --git a/src/vs/base/test/common/processes.test.ts b/src/vs/base/test/common/processes.test.ts index d575590ab10..6c0d88e62bb 100644 --- a/src/vs/base/test/common/processes.test.ts +++ b/src/vs/base/test/common/processes.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as processes from 'vs/base/common/processes'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/resourceTree.test.ts b/src/vs/base/test/common/resourceTree.test.ts index 9024269a1ef..51fba68f764 100644 --- a/src/vs/base/test/common/resourceTree.test.ts +++ b/src/vs/base/test/common/resourceTree.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ResourceTree } from 'vs/base/common/resourceTree'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/resources.test.ts b/src/vs/base/test/common/resources.test.ts index d1e4e1025ae..d2315644376 100644 --- a/src/vs/base/test/common/resources.test.ts +++ b/src/vs/base/test/common/resources.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { toSlashes } from 'vs/base/common/extpath'; import { posix, win32 } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/base/test/common/scrollable.test.ts b/src/vs/base/test/common/scrollable.test.ts index 7059d813daa..41a33727b6e 100644 --- a/src/vs/base/test/common/scrollable.test.ts +++ b/src/vs/base/test/common/scrollable.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { SmoothScrollingOperation, SmoothScrollingUpdate } from 'vs/base/common/scrollable'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/skipList.test.ts b/src/vs/base/test/common/skipList.test.ts index 8dadb0e34dd..096bd6a268f 100644 --- a/src/vs/base/test/common/skipList.test.ts +++ b/src/vs/base/test/common/skipList.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { binarySearch } from 'vs/base/common/arrays'; import { SkipList } from 'vs/base/common/skipList'; import { StopWatch } from 'vs/base/common/stopwatch'; diff --git a/src/vs/base/test/common/stream.test.ts b/src/vs/base/test/common/stream.test.ts index 5589dfdceaf..a8473f0ff8f 100644 --- a/src/vs/base/test/common/stream.test.ts +++ b/src/vs/base/test/common/stream.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { bufferToReadable, VSBuffer } from 'vs/base/common/buffer'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index 4be439f4656..b84a193eba1 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as strings from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/ternarySearchtree.test.ts b/src/vs/base/test/common/ternarySearchtree.test.ts index c1a8cc771a1..1e684933d46 100644 --- a/src/vs/base/test/common/ternarySearchtree.test.ts +++ b/src/vs/base/test/common/ternarySearchtree.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { shuffle } from 'vs/base/common/arrays'; import { randomPath } from 'vs/base/common/extpath'; import { StopWatch } from 'vs/base/common/stopwatch'; diff --git a/src/vs/base/test/common/tfIdf.test.ts b/src/vs/base/test/common/tfIdf.test.ts index 3a423f253a3..df5e37da270 100644 --- a/src/vs/base/test/common/tfIdf.test.ts +++ b/src/vs/base/test/common/tfIdf.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { TfIdfCalculator, TfIdfDocument, TfIdfScore } from 'vs/base/common/tfIdf'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/types.test.ts b/src/vs/base/test/common/types.test.ts index 93e2464925a..f9b894fb73a 100644 --- a/src/vs/base/test/common/types.test.ts +++ b/src/vs/base/test/common/types.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as types from 'vs/base/common/types'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/uri.test.ts b/src/vs/base/test/common/uri.test.ts index 69e2abe720d..fa65209a568 100644 --- a/src/vs/base/test/common/uri.test.ts +++ b/src/vs/base/test/common/uri.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; import { URI, UriComponents, isUriComponents } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/uuid.test.ts b/src/vs/base/test/common/uuid.test.ts index a8defebb567..1cfbf5c12e8 100644 --- a/src/vs/base/test/common/uuid.test.ts +++ b/src/vs/base/test/common/uuid.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as uuid from 'vs/base/common/uuid'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/node/css.build.test.ts b/src/vs/base/test/node/css.build.test.ts index 16c79d41225..4f824b375d9 100644 --- a/src/vs/base/test/node/css.build.test.ts +++ b/src/vs/base/test/node/css.build.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CSSPluginUtilities, rewriteUrls } from 'vs/css.build'; diff --git a/src/vs/base/test/node/extpath.test.ts b/src/vs/base/test/node/extpath.test.ts index 04aa1873295..b7d78dbee46 100644 --- a/src/vs/base/test/node/extpath.test.ts +++ b/src/vs/base/test/node/extpath.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { tmpdir } from 'os'; import { realcase, realcaseSync, realpath, realpathSync } from 'vs/base/node/extpath'; import { Promises } from 'vs/base/node/pfs'; diff --git a/src/vs/base/test/node/id.test.ts b/src/vs/base/test/node/id.test.ts index 1a629134f06..259b2312c1d 100644 --- a/src/vs/base/test/node/id.test.ts +++ b/src/vs/base/test/node/id.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { getMachineId, getSqmMachineId, getdevDeviceId } from 'vs/base/node/id'; import { getMac } from 'vs/base/node/macAddress'; import { flakySuite } from 'vs/base/test/node/testUtils'; diff --git a/src/vs/base/test/node/nodeStreams.test.ts b/src/vs/base/test/node/nodeStreams.test.ts index 8cab1cc8420..44e7dd9154d 100644 --- a/src/vs/base/test/node/nodeStreams.test.ts +++ b/src/vs/base/test/node/nodeStreams.test.ts @@ -5,7 +5,7 @@ import { Writable } from 'stream'; -import * as assert from 'assert'; +import assert from 'assert'; import { StreamSplitter } from 'vs/base/node/nodeStreams'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index b3ef62f232a..8fec28d2bfc 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import { tmpdir } from 'os'; import { timeout } from 'vs/base/common/async'; diff --git a/src/vs/base/test/node/port.test.ts b/src/vs/base/test/node/port.test.ts index 9230dc84684..048cfc69bd1 100644 --- a/src/vs/base/test/node/port.test.ts +++ b/src/vs/base/test/node/port.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as net from 'net'; import * as ports from 'vs/base/node/ports'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/node/powershell.test.ts b/src/vs/base/test/node/powershell.test.ts index a710bb80c46..de9b669c769 100644 --- a/src/vs/base/test/node/powershell.test.ts +++ b/src/vs/base/test/node/powershell.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import * as platform from 'vs/base/common/platform'; import { enumeratePowerShellInstallations, getFirstAvailablePowerShellInstallation, IPowerShellExeDetails } from 'vs/base/node/powershell'; diff --git a/src/vs/base/test/node/processes/processes.integrationTest.ts b/src/vs/base/test/node/processes/processes.integrationTest.ts index ff52bf0205c..f8e827a8594 100644 --- a/src/vs/base/test/node/processes/processes.integrationTest.ts +++ b/src/vs/base/test/node/processes/processes.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as cp from 'child_process'; import { FileAccess } from 'vs/base/common/network'; import * as objects from 'vs/base/common/objects'; diff --git a/src/vs/base/test/node/uri.perf.test.ts b/src/vs/base/test/node/uri.perf.test.ts index 389f3c999dd..cd045f15d0b 100644 --- a/src/vs/base/test/node/uri.perf.test.ts +++ b/src/vs/base/test/node/uri.perf.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { readFileSync } from 'fs'; import { FileAccess } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/base/test/node/zip/zip.test.ts b/src/vs/base/test/node/zip/zip.test.ts index 0a898e2c7bc..bde3eaf7482 100644 --- a/src/vs/base/test/node/zip/zip.test.ts +++ b/src/vs/base/test/node/zip/zip.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { tmpdir } from 'os'; import { createCancelablePromise } from 'vs/base/common/async'; import { FileAccess } from 'vs/base/common/network'; diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index bf97eda3640..2a969e3a2b6 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -12,7 +12,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isSigPipeError, onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { isEqualOrParent } from 'vs/base/common/extpath'; import { Event } from 'vs/base/common/event'; -import { stripComments } from 'vs/base/common/json'; +import { parse } from 'vs/base/common/jsonc'; import { getPathLabel } from 'vs/base/common/labels'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas, VSCODE_AUTHORITY } from 'vs/base/common/network'; @@ -52,8 +52,9 @@ import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemPro import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IIssueMainService } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, IProcessMainService } from 'vs/platform/issue/common/issue'; import { IssueMainService } from 'vs/platform/issue/electron-main/issueMainService'; +import { ProcessMainService } from 'vs/platform/issue/electron-main/processMainService'; import { IKeyboardLayoutMainService, KeyboardLayoutMainService } from 'vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService'; import { ILaunchMainService, LaunchMainService } from 'vs/platform/launch/electron-main/launchMainService'; import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; @@ -121,7 +122,6 @@ import { Lazy } from 'vs/base/common/lazy'; import { IAuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows'; import { AuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService'; import { normalizeNFC } from 'vs/base/common/normalization'; - /** * The main VS Code application. There will only ever be one instance, * even if the user starts many instances (e.g. from the command line). @@ -370,7 +370,7 @@ export class CodeApplication extends Disposable { process.on('unhandledRejection', (reason: unknown) => onUnexpectedError(reason)); // Dispose on shutdown - this.lifecycleMainService.onWillShutdown(() => this.dispose()); + Event.once(this.lifecycleMainService.onWillShutdown)(() => this.dispose()); // Contextmenu via IPC support registerContextMenuListener(); @@ -604,7 +604,7 @@ export class CodeApplication extends Disposable { // Main process server (electron IPC based) const mainProcessElectronServer = new ElectronIPCServer(); - this.lifecycleMainService.onWillShutdown(e => { + Event.once(this.lifecycleMainService.onWillShutdown)(e => { if (e.reason === ShutdownReason.KILL) { // When we go down abnormally, make sure to free up // any IPC we accept from other windows to reduce @@ -1057,6 +1057,9 @@ export class CodeApplication extends Disposable { // Issues services.set(IIssueMainService, new SyncDescriptor(IssueMainService, [this.userEnv])); + // Process + services.set(IProcessMainService, new SyncDescriptor(ProcessMainService, [this.userEnv])); + // Encryption services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService)); @@ -1189,6 +1192,10 @@ export class CodeApplication extends Disposable { const issueChannel = ProxyChannel.fromService(accessor.get(IIssueMainService), disposables); mainProcessElectronServer.registerChannel('issue', issueChannel); + // Process + const processChannel = ProxyChannel.fromService(accessor.get(IProcessMainService), disposables); + mainProcessElectronServer.registerChannel('process', processChannel); + // Encryption const encryptionChannel = ProxyChannel.fromService(accessor.get(IEncryptionMainService), disposables); mainProcessElectronServer.registerChannel('encryption', encryptionChannel); @@ -1400,10 +1407,10 @@ export class CodeApplication extends Disposable { // Crash reporter this.updateCrashReporterEnablement(); + // macOS: rosetta translation warning if (isMacintosh && app.runningUnderARM64Translation) { this.windowsMainService?.sendToFocused('vscode:showTranslatedBuildWarning'); } - } private async installMutex(): Promise { @@ -1443,7 +1450,7 @@ export class CodeApplication extends Disposable { try { const argvContent = await this.fileService.readFile(this.environmentMainService.argvResource); const argvString = argvContent.value.toString(); - const argvJSON = JSON.parse(stripComments(argvString)); + const argvJSON = parse(argvString); const telemetryLevel = getTelemetryLevel(this.configurationService); const enableCrashReporter = telemetryLevel >= TelemetryLevel.CRASH; @@ -1474,6 +1481,9 @@ export class CodeApplication extends Disposable { } } catch (error) { this.logService.error(error); + + // Inform the user via notification + this.windowsMainService?.sendToFocused('vscode:showArgvParseWarning'); } } } diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js index 8234b734d06..a81bd8c04ed 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js @@ -7,6 +7,10 @@ (function () { 'use strict'; + /** + * @import { ISandboxConfiguration } from '../../../base/parts/sandbox/common/sandboxTypes' + */ + const bootstrapWindow = bootstrapWindowLib(); // Load process explorer into window @@ -21,12 +25,10 @@ }); /** - * @typedef {import('../../../base/parts/sandbox/common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration - * * @returns {{ * load: ( * modules: string[], - * resultCallback: (result, configuration: ISandboxConfiguration) => unknown, + * resultCallback: (result: any, configuration: ISandboxConfiguration) => unknown, * options?: { * configureDeveloperSettings?: (config: ISandboxConfiguration) => { * forceEnableDeveloperKeybindings?: boolean, diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js index 35e8368d3c9..25830623237 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.js +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -9,6 +9,12 @@ (function () { 'use strict'; + /** + * @import {INativeWindowConfiguration} from '../../../platform/window/common/window' + * @import {NativeParsedArgs} from '../../../platform/environment/common/argv' + * @import {ISandboxConfiguration} from '../../../base/parts/sandbox/common/sandboxTypes' + */ + const bootstrapWindow = bootstrapWindowLib(); // Add a perf entry right from the top @@ -45,6 +51,7 @@ showSplash(windowConfig); }, beforeLoaderConfig: function (loaderConfig) { + // @ts-ignore loaderConfig.recordStats = true; }, beforeRequire: function (windowConfig) { @@ -74,14 +81,10 @@ //#region Helpers /** - * @typedef {import('../../../platform/window/common/window').INativeWindowConfiguration} INativeWindowConfiguration - * @typedef {import('../../../platform/environment/common/argv').NativeParsedArgs} NativeParsedArgs - * @typedef {import('../../../base/parts/sandbox/common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration - * * @returns {{ * load: ( * modules: string[], - * resultCallback: (result, configuration: INativeWindowConfiguration & NativeParsedArgs) => unknown, + * resultCallback: (result: any, configuration: INativeWindowConfiguration & NativeParsedArgs) => unknown, * options?: { * configureDeveloperSettings?: (config: INativeWindowConfiguration & NativeParsedArgs) => { * forceDisableShowDevtoolsOnError?: boolean, @@ -129,7 +132,9 @@ } // minimal color configuration (works with or without persisted data) - let baseTheme, shellBackground, shellForeground; + let baseTheme; + let shellBackground; + let shellForeground; if (data) { baseTheme = data.baseTheme; shellBackground = data.colorInfo.editorBackground; @@ -162,7 +167,9 @@ style.textContent = `body { background-color: ${shellBackground}; color: ${shellForeground}; margin: 0; padding: 0; }`; // set zoom level as soon as possible + // @ts-ignore if (typeof data?.zoomLevel === 'number' && typeof globalThis.vscode?.webFrame?.setZoomLevel === 'function') { + // @ts-ignore globalThis.vscode.webFrame.setZoomLevel(data.zoomLevel); } @@ -172,9 +179,9 @@ const splash = document.createElement('div'); splash.id = 'monaco-parts-splash'; - splash.className = baseTheme; + splash.className = baseTheme ?? 'vs-dark'; - if (layoutInfo.windowBorder) { + if (layoutInfo.windowBorder && colorInfo.windowBorder) { splash.style.position = 'relative'; splash.style.height = 'calc(100vh - 2px)'; splash.style.width = 'calc(100vw - 2px)'; diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index f8e915491fc..69c81ac2498 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -71,7 +71,7 @@ import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyn import { UserDataSyncServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncServiceIpc'; import { UserDataSyncStoreManagementService, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; -import { NativeUserDataProfileStorageService } from 'vs/platform/userDataProfile/node/userDataProfileStorageService'; +import { SharedProcessUserDataProfileStorageService } from 'vs/platform/userDataProfile/node/userDataProfileStorageService'; import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/node/signService'; @@ -355,7 +355,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { services.set(IUserDataSyncLocalStoreService, new SyncDescriptor(UserDataSyncLocalStoreService, undefined, false /* Eagerly cleans up old backups */)); services.set(IUserDataSyncEnablementService, new SyncDescriptor(UserDataSyncEnablementService, undefined, true)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService, undefined, false /* Initializes the Sync State */)); - services.set(IUserDataProfileStorageService, new SyncDescriptor(NativeUserDataProfileStorageService, undefined, true)); + services.set(IUserDataProfileStorageService, new SyncDescriptor(SharedProcessUserDataProfileStorageService, undefined, true)); services.set(IUserDataSyncResourceProviderService, new SyncDescriptor(UserDataSyncResourceProviderService, undefined, true)); // Signing diff --git a/src/vs/editor/browser/config/charWidthReader.ts b/src/vs/editor/browser/config/charWidthReader.ts index 90bafb66284..e1d36f142bf 100644 --- a/src/vs/editor/browser/config/charWidthReader.ts +++ b/src/vs/editor/browser/config/charWidthReader.ts @@ -56,7 +56,7 @@ class DomCharWidthReader { this._readFromDomElements(); // Remove the container from the DOM - targetWindow.document.body.removeChild(this._container!); + this._container?.remove(); this._container = null; this._testElements = null; diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index c8e5b7e50a4..a5f824ca450 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -950,7 +950,7 @@ function measureText(targetDocument: Document, text: string, fontInfo: FontInfo, const res = regularDomNode.offsetWidth; - targetDocument.body.removeChild(container); + container.remove(); return res; } diff --git a/src/vs/editor/browser/coreCommands.ts b/src/vs/editor/browser/coreCommands.ts index ca0a4bb8a1f..e7f0743af83 100644 --- a/src/vs/editor/browser/coreCommands.ts +++ b/src/vs/editor/browser/coreCommands.ts @@ -30,6 +30,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IViewModel } from 'vs/editor/common/viewModel'; import { ISelection } from 'vs/editor/common/core/selection'; import { getActiveElement } from 'vs/base/browser/dom'; +import { EnterOperation } from 'vs/editor/common/cursor/cursorTypeEditOperations'; const CORE_WEIGHT = KeybindingWeight.EditorCore; @@ -74,7 +75,7 @@ export namespace EditorScroll_ { return true; }; - export const metadata = { + export const metadata: ICommandMetadata = { description: 'Scroll editor in the given direction', args: [ { @@ -252,7 +253,7 @@ export namespace RevealLine_ { return true; }; - export const metadata = { + export const metadata: ICommandMetadata = { description: 'Reveal the given line at the given logical position', args: [ { @@ -1988,7 +1989,7 @@ export namespace CoreEditingCommands { public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: unknown): void { editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.lineBreakInsert(viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection))); + editor.executeCommands(this.id, EnterOperation.lineBreakInsert(viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection))); } }); diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts new file mode 100644 index 00000000000..195aee2cf4f --- /dev/null +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -0,0 +1,282 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { equalsIfDefined, itemsEquals } from 'vs/base/common/equals'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, ITransaction, autorun, autorunOpts, autorunWithStoreHandleChanges, derived, derivedOpts, observableFromEvent, observableSignal, observableValue, observableValueOpts } from 'vs/base/common/observable'; +import { TransactionImpl } from 'vs/base/common/observableInternal/base'; +import { derivedWithSetter } from 'vs/base/common/observableInternal/derived'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { EditorOption, FindComputedEditorOptionValueById } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ICursorSelectionChangedEvent } from 'vs/editor/common/cursorEvents'; +import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; + +/** + * Returns a facade for the code editor that provides observables for various states/events. +*/ +export function observableCodeEditor(editor: ICodeEditor): ObservableCodeEditor { + return ObservableCodeEditor.get(editor); +} + +export class ObservableCodeEditor extends Disposable { + private static readonly _map = new Map(); + + /** + * Make sure that editor is not disposed yet! + */ + public static get(editor: ICodeEditor): ObservableCodeEditor { + let result = ObservableCodeEditor._map.get(editor); + if (!result) { + result = new ObservableCodeEditor(editor); + ObservableCodeEditor._map.set(editor, result); + const d = editor.onDidDispose(() => { + const item = ObservableCodeEditor._map.get(editor); + if (item) { + ObservableCodeEditor._map.delete(editor); + item.dispose(); + d.dispose(); + } + }); + } + return result; + } + + private _updateCounter = 0; + private _currentTransaction: TransactionImpl | undefined = undefined; + + private _beginUpdate(): void { + this._updateCounter++; + if (this._updateCounter === 1) { + this._currentTransaction = new TransactionImpl(() => { + /** @description Update editor state */ + }); + } + } + + private _endUpdate(): void { + this._updateCounter--; + if (this._updateCounter === 0) { + const t = this._currentTransaction!; + this._currentTransaction = undefined; + t.finish(); + } + } + + private constructor(public readonly editor: ICodeEditor) { + super(); + + this._register(this.editor.onBeginUpdate(() => this._beginUpdate())); + this._register(this.editor.onEndUpdate(() => this._endUpdate())); + + this._register(this.editor.onDidChangeModel(() => { + this._beginUpdate(); + try { + this._model.set(this.editor.getModel(), this._currentTransaction); + this._forceUpdate(); + } finally { + this._endUpdate(); + } + })); + + this._register(this.editor.onDidType((e) => { + this._beginUpdate(); + try { + this._forceUpdate(); + this.onDidType.trigger(this._currentTransaction, e); + } finally { + this._endUpdate(); + } + })); + + this._register(this.editor.onDidChangeModelContent(e => { + this._beginUpdate(); + try { + this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, e); + this._forceUpdate(); + } finally { + this._endUpdate(); + } + })); + + this._register(this.editor.onDidChangeCursorSelection(e => { + this._beginUpdate(); + try { + this._selections.set(this.editor.getSelections(), this._currentTransaction, e); + this._forceUpdate(); + } finally { + this._endUpdate(); + } + })); + } + + public forceUpdate(): void; + public forceUpdate(cb: (tx: ITransaction) => T): T; + public forceUpdate(cb?: (tx: ITransaction) => T): T { + this._beginUpdate(); + try { + this._forceUpdate(); + if (!cb) { return undefined as T; } + return cb(this._currentTransaction!); + } finally { + this._endUpdate(); + } + } + + private _forceUpdate(): void { + this._beginUpdate(); + try { + this._model.set(this.editor.getModel(), this._currentTransaction); + this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, undefined); + this._selections.set(this.editor.getSelections(), this._currentTransaction, undefined); + } finally { + this._endUpdate(); + } + } + + private readonly _model = observableValue(this, this.editor.getModel()); + public readonly model: IObservable = this._model; + + public readonly isReadonly = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly)); + + private readonly _versionId = observableValueOpts({ owner: this, lazy: true }, this.editor.getModel()?.getVersionId() ?? null); + public readonly versionId: IObservable = this._versionId; + + private readonly _selections = observableValueOpts( + { owner: this, equalsFn: equalsIfDefined(itemsEquals(Selection.selectionsEqual)), lazy: true }, + this.editor.getSelections() ?? null + ); + public readonly selections: IObservable = this._selections; + + + public readonly positions = derivedOpts( + { owner: this, equalsFn: equalsIfDefined(itemsEquals(Position.equals)) }, + reader => this.selections.read(reader)?.map(s => s.getStartPosition()) ?? null + ); + + public readonly isFocused = observableFromEvent(this, e => { + const d1 = this.editor.onDidFocusEditorWidget(e); + const d2 = this.editor.onDidBlurEditorWidget(e); + return { + dispose() { + d1.dispose(); + d2.dispose(); + } + }; + }, () => this.editor.hasWidgetFocus()); + + public readonly value = derivedWithSetter(this, + reader => { this.versionId.read(reader); return this.model.read(reader)?.getValue() ?? ''; }, + (value, tx) => { + const model = this.model.get(); + if (model !== null) { + if (value !== model.getValue()) { + model.setValue(value); + } + } + } + ); + public readonly valueIsEmpty = derived(this, reader => { this.versionId.read(reader); return this.editor.getModel()?.getValueLength() === 0; }); + public readonly cursorSelection = derivedOpts({ owner: this, equalsFn: equalsIfDefined(Selection.selectionsEqual) }, reader => this.selections.read(reader)?.[0] ?? null); + public readonly cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null); + + public readonly onDidType = observableSignal(this); + + public readonly scrollTop = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollTop()); + public readonly scrollLeft = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollLeft()); + + public readonly layoutInfo = observableFromEvent(this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo()); + + public readonly contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth()); + + public getOption(id: T): IObservable> { + return observableFromEvent(this, cb => this.editor.onDidChangeConfiguration(e => { + if (e.hasChanged(id)) { cb(undefined); } + }), () => this.editor.getOption(id)); + } + + public setDecorations(decorations: IObservable): IDisposable { + const d = new DisposableStore(); + const decorationsCollection = this.editor.createDecorationsCollection(); + d.add(autorunOpts({ owner: this, debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => { + const d = decorations.read(reader); + decorationsCollection.set(d); + })); + d.add({ + dispose: () => { + decorationsCollection.clear(); + } + }); + return d; + } + + private _overlayWidgetCounter = 0; + + public createOverlayWidget(widget: IObservableOverlayWidget): IDisposable { + const overlayWidgetId = 'observableOverlayWidget' + (this._overlayWidgetCounter++); + const w: IOverlayWidget = { + getDomNode: () => widget.domNode, + getPosition: () => widget.position.get(), + getId: () => overlayWidgetId, + allowEditorOverflow: widget.allowEditorOverflow, + getMinContentWidthInPx: () => widget.minContentWidthInPx.get(), + }; + this.editor.addOverlayWidget(w); + const d = autorun(reader => { + widget.position.read(reader); + widget.minContentWidthInPx.read(reader); + this.editor.layoutOverlayWidget(w); + }); + return toDisposable(() => { + d.dispose(); + this.editor.removeOverlayWidget(w); + }); + } +} + +interface IObservableOverlayWidget { + get domNode(): HTMLElement; + readonly position: IObservable; + readonly minContentWidthInPx: IObservable; + get allowEditorOverflow(): boolean; +} + +type RemoveUndefined = T extends undefined ? never : T; +export function reactToChange(observable: IObservable, cb: (value: T, deltas: RemoveUndefined[]) => void): IDisposable { + return autorunWithStoreHandleChanges({ + createEmptyChangeSummary: () => ({ deltas: [] as RemoveUndefined[], didChange: false }), + handleChange: (context, changeSummary) => { + if (context.didChange(observable)) { + const e = context.change; + if (e !== undefined) { + changeSummary.deltas.push(e as RemoveUndefined); + } + changeSummary.didChange = true; + } + return true; + }, + }, (reader, changeSummary) => { + const value = observable.read(reader); + if (changeSummary.didChange) { + cb(value, changeSummary.deltas); + } + }); +} + +export function reactToChangeWithStore(observable: IObservable, cb: (value: T, deltas: RemoveUndefined[], store: DisposableStore) => void): IDisposable { + const store = new DisposableStore(); + const disposable = reactToChange(observable, (value, deltas) => { + store.clear(); + cb(value, deltas, store); + }); + return { + dispose() { + disposable.dispose(); + store.dispose(); + } + }; +} diff --git a/src/vs/editor/browser/observableUtilities.ts b/src/vs/editor/browser/observableUtilities.ts deleted file mode 100644 index 7bcfe7ecd6a..00000000000 --- a/src/vs/editor/browser/observableUtilities.ts +++ /dev/null @@ -1,71 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { autorunOpts, derivedOpts, IObservable, observableFromEvent } from 'vs/base/common/observable'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { Position } from 'vs/editor/common/core/position'; -import { IModelDeltaDecoration } from 'vs/editor/common/model'; - -/** - * Returns a facade for the code editor that provides observables for various states/events. -*/ -export function obsCodeEditor(editor: ICodeEditor): ObservableCodeEditor { - return ObservableCodeEditor.get(editor); -} - -class ObservableCodeEditor { - private static _map = new Map(); - - /** - * Make sure that editor is not disposed yet! - */ - public static get(editor: ICodeEditor): ObservableCodeEditor { - let result = ObservableCodeEditor._map.get(editor); - if (!result) { - result = new ObservableCodeEditor(editor); - ObservableCodeEditor._map.set(editor, result); - const d = editor.onDidDispose(() => { - ObservableCodeEditor._map.delete(editor); - d.dispose(); - }); - } - return result; - } - - private constructor(public readonly editor: ICodeEditor) { - } - - public readonly model = observableFromEvent(this.editor.onDidChangeModel, () => this.editor.getModel()); - public readonly value = observableFromEvent(this.editor.onDidChangeModelContent, () => this.editor.getValue()); - public readonly valueIsEmpty = observableFromEvent(this.editor.onDidChangeModelContent, () => this.editor.getModel()?.getValueLength() === 0); - public readonly selections = observableFromEvent(this.editor.onDidChangeCursorSelection, () => this.editor.getSelections()); - public readonly cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null); - public readonly isFocused = observableFromEvent(e => { - const d1 = this.editor.onDidFocusEditorWidget(e); - const d2 = this.editor.onDidBlurEditorWidget(e); - return { - dispose() { - d1.dispose(); - d2.dispose(); - } - }; - }, () => this.editor.hasWidgetFocus()); - - public setDecorations(decorations: IObservable): IDisposable { - const d = new DisposableStore(); - const decorationsCollection = this.editor.createDecorationsCollection(); - d.add(autorunOpts({ owner: this, debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => { - const d = decorations.read(reader); - decorationsCollection.set(d); - })); - d.add({ - dispose: () => { - decorationsCollection.clear(); - } - }); - return d; - } -} diff --git a/src/vs/editor/browser/services/abstractCodeEditorService.ts b/src/vs/editor/browser/services/abstractCodeEditorService.ts index b960f48191e..1fedb4d17c4 100644 --- a/src/vs/editor/browser/services/abstractCodeEditorService.ts +++ b/src/vs/editor/browser/services/abstractCodeEditorService.ts @@ -341,7 +341,7 @@ class RefCountedStyleSheet { public unref(): void { this._refCount--; if (this._refCount === 0) { - this._styleSheet.parentNode?.removeChild(this._styleSheet); + this._styleSheet.remove(); this._parent._removeEditorStyleSheets(this._editorId); } } diff --git a/src/vs/editor/browser/services/hoverService/hoverService.ts b/src/vs/editor/browser/services/hoverService/hoverService.ts index 0c554ca039e..20faf6cfa8c 100644 --- a/src/vs/editor/browser/services/hoverService/hoverService.ts +++ b/src/vs/editor/browser/services/hoverService/hoverService.ts @@ -20,9 +20,9 @@ import { IAccessibilityService } from 'vs/platform/accessibility/common/accessib import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { mainWindow } from 'vs/base/browser/window'; import { ContextViewHandler } from 'vs/platform/contextview/browser/contextViewService'; -import type { IHoverOptions, IHoverWidget, IUpdatableHover, IUpdatableHoverContentOrFactory, IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover'; +import type { IHoverOptions, IHoverWidget, IManagedHover, IManagedHoverContentOrFactory, IManagedHoverOptions } from 'vs/base/browser/ui/hover/hover'; import type { IHoverDelegate, IHoverDelegateTarget } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { UpdatableHoverWidget } from 'vs/editor/browser/services/hoverService/updatableHoverWidget'; +import { ManagedHoverWidget } from 'vs/editor/browser/services/hoverService/updatableHoverWidget'; import { TimeoutTimer } from 'vs/base/common/async'; export class HoverService extends Disposable implements IHoverService { @@ -189,22 +189,22 @@ export class HoverService extends Disposable implements IHoverService { } } - private readonly _existingHovers = new Map(); + private readonly _managedHovers = new Map(); // TODO: Investigate performance of this function. There seems to be a lot of content created // and thrown away on start up - setupUpdatableHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, content: IUpdatableHoverContentOrFactory, options?: IUpdatableHoverOptions | undefined): IUpdatableHover { + setupManagedHover(hoverDelegate: IHoverDelegate, targetElement: HTMLElement, content: IManagedHoverContentOrFactory, options?: IManagedHoverOptions | undefined): IManagedHover { - htmlElement.setAttribute('custom-hover', 'true'); + targetElement.setAttribute('custom-hover', 'true'); - if (htmlElement.title !== '') { + if (targetElement.title !== '') { console.warn('HTML element already has a title attribute, which will conflict with the custom hover. Please remove the title attribute.'); - console.trace('Stack trace:', htmlElement.title); - htmlElement.title = ''; + console.trace('Stack trace:', targetElement.title); + targetElement.title = ''; } let hoverPreparation: IDisposable | undefined; - let hoverWidget: UpdatableHoverWidget | undefined; + let hoverWidget: ManagedHoverWidget | undefined; const hideHover = (disposeWidget: boolean, disposePreparation: boolean) => { const hadHover = hoverWidget !== undefined; @@ -225,23 +225,23 @@ export class HoverService extends Disposable implements IHoverService { const triggerShowHover = (delay: number, focus?: boolean, target?: IHoverDelegateTarget, trapFocus?: boolean) => { return new TimeoutTimer(async () => { if (!hoverWidget || hoverWidget.isDisposed) { - hoverWidget = new UpdatableHoverWidget(hoverDelegate, target || htmlElement, delay > 0); + hoverWidget = new ManagedHoverWidget(hoverDelegate, target || targetElement, delay > 0); await hoverWidget.update(typeof content === 'function' ? content() : content, focus, { ...options, trapFocus }); } }, delay); }; let isMouseDown = false; - const mouseDownEmitter = addDisposableListener(htmlElement, EventType.MOUSE_DOWN, () => { + const mouseDownEmitter = addDisposableListener(targetElement, EventType.MOUSE_DOWN, () => { isMouseDown = true; hideHover(true, true); }, true); - const mouseUpEmitter = addDisposableListener(htmlElement, EventType.MOUSE_UP, () => { + const mouseUpEmitter = addDisposableListener(targetElement, EventType.MOUSE_UP, () => { isMouseDown = false; }, true); - const mouseLeaveEmitter = addDisposableListener(htmlElement, EventType.MOUSE_LEAVE, (e: MouseEvent) => { + const mouseLeaveEmitter = addDisposableListener(targetElement, EventType.MOUSE_LEAVE, (e: MouseEvent) => { isMouseDown = false; - hideHover(false, (e).fromElement === htmlElement); + hideHover(false, (e).fromElement === targetElement); }, true); const onMouseOver = (e: MouseEvent) => { @@ -252,53 +252,53 @@ export class HoverService extends Disposable implements IHoverService { const toDispose: DisposableStore = new DisposableStore(); const target: IHoverDelegateTarget = { - targetElements: [htmlElement], + targetElements: [targetElement], dispose: () => { } }; if (hoverDelegate.placement === undefined || hoverDelegate.placement === 'mouse') { // track the mouse position const onMouseMove = (e: MouseEvent) => { target.x = e.x + 10; - if ((isHTMLElement(e.target)) && getHoverTargetElement(e.target, htmlElement) !== htmlElement) { + if ((isHTMLElement(e.target)) && getHoverTargetElement(e.target, targetElement) !== targetElement) { hideHover(true, true); } }; - toDispose.add(addDisposableListener(htmlElement, EventType.MOUSE_MOVE, onMouseMove, true)); + toDispose.add(addDisposableListener(targetElement, EventType.MOUSE_MOVE, onMouseMove, true)); } hoverPreparation = toDispose; - if ((isHTMLElement(e.target)) && getHoverTargetElement(e.target as HTMLElement, htmlElement) !== htmlElement) { + if ((isHTMLElement(e.target)) && getHoverTargetElement(e.target as HTMLElement, targetElement) !== targetElement) { return; // Do not show hover when the mouse is over another hover target } toDispose.add(triggerShowHover(hoverDelegate.delay, false, target)); }; - const mouseOverDomEmitter = addDisposableListener(htmlElement, EventType.MOUSE_OVER, onMouseOver, true); + const mouseOverDomEmitter = addDisposableListener(targetElement, EventType.MOUSE_OVER, onMouseOver, true); const onFocus = () => { if (isMouseDown || hoverPreparation) { return; } const target: IHoverDelegateTarget = { - targetElements: [htmlElement], + targetElements: [targetElement], dispose: () => { } }; const toDispose: DisposableStore = new DisposableStore(); const onBlur = () => hideHover(true, true); - toDispose.add(addDisposableListener(htmlElement, EventType.BLUR, onBlur, true)); + toDispose.add(addDisposableListener(targetElement, EventType.BLUR, onBlur, true)); toDispose.add(triggerShowHover(hoverDelegate.delay, false, target)); hoverPreparation = toDispose; }; // Do not show hover when focusing an input or textarea let focusDomEmitter: undefined | IDisposable; - const tagName = htmlElement.tagName.toLowerCase(); + const tagName = targetElement.tagName.toLowerCase(); if (tagName !== 'input' && tagName !== 'textarea') { - focusDomEmitter = addDisposableListener(htmlElement, EventType.FOCUS, onFocus, true); + focusDomEmitter = addDisposableListener(targetElement, EventType.FOCUS, onFocus, true); } - const hover: IUpdatableHover = { + const hover: IManagedHover = { show: focus => { hideHover(false, true); // terminate a ongoing mouse over preparation triggerShowHover(0, focus, undefined, focus); // show hover immediately @@ -311,7 +311,7 @@ export class HoverService extends Disposable implements IHoverService { await hoverWidget?.update(content, undefined, hoverOptions); }, dispose: () => { - this._existingHovers.delete(htmlElement); + this._managedHovers.delete(targetElement); mouseOverDomEmitter.dispose(); mouseLeaveEmitter.dispose(); mouseDownEmitter.dispose(); @@ -320,19 +320,19 @@ export class HoverService extends Disposable implements IHoverService { hideHover(true, true); } }; - this._existingHovers.set(htmlElement, hover); + this._managedHovers.set(targetElement, hover); return hover; } - triggerUpdatableHover(target: HTMLElement): void { - const hover = this._existingHovers.get(target); + showManagedHover(target: HTMLElement): void { + const hover = this._managedHovers.get(target); if (hover) { hover.show(true); } } public override dispose(): void { - this._existingHovers.forEach(hover => hover.dispose()); + this._managedHovers.forEach(hover => hover.dispose()); super.dispose(); } } diff --git a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts index 3b746de6f43..cf9b355831f 100644 --- a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts +++ b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isHTMLElement } from 'vs/base/browser/dom'; -import type { IHoverWidget, IUpdatableHoverContent, IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover'; +import type { IHoverWidget, IManagedHoverContent, IManagedHoverOptions } from 'vs/base/browser/ui/hover/hover'; import type { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/hover/hoverDelegate'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -13,9 +13,9 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { isFunction, isString } from 'vs/base/common/types'; import { localize } from 'vs/nls'; -type IUpdatableHoverResolvedContent = IMarkdownString | string | HTMLElement | undefined; +type IManagedHoverResolvedContent = IMarkdownString | string | HTMLElement | undefined; -export class UpdatableHoverWidget implements IDisposable { +export class ManagedHoverWidget implements IDisposable { private _hoverWidget: IHoverWidget | undefined; private _cancellationTokenSource: CancellationTokenSource | undefined; @@ -23,7 +23,7 @@ export class UpdatableHoverWidget implements IDisposable { constructor(private hoverDelegate: IHoverDelegate, private target: IHoverDelegateTarget | HTMLElement, private fadeInAnimation: boolean) { } - async update(content: IUpdatableHoverContent, focus?: boolean, options?: IUpdatableHoverOptions): Promise { + async update(content: IManagedHoverContent, focus?: boolean, options?: IManagedHoverOptions): Promise { if (this._cancellationTokenSource) { // there's an computation ongoing, cancel it this._cancellationTokenSource.dispose(true); @@ -64,7 +64,7 @@ export class UpdatableHoverWidget implements IDisposable { this.show(resolvedContent, focus, options); } - private show(content: IUpdatableHoverResolvedContent, focus?: boolean, options?: IUpdatableHoverOptions): void { + private show(content: IManagedHoverResolvedContent, focus?: boolean, options?: IManagedHoverOptions): void { const oldHoverWidget = this._hoverWidget; if (this.hasContent(content)) { @@ -86,7 +86,7 @@ export class UpdatableHoverWidget implements IDisposable { oldHoverWidget?.dispose(); } - private hasContent(content: IUpdatableHoverResolvedContent): content is NonNullable { + private hasContent(content: IManagedHoverResolvedContent): content is NonNullable { if (!content) { return false; } diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 64fb2185eed..2861fd8f82a 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -184,7 +184,7 @@ function createLineBreaks(targetWindow: Window, requests: string[], fontInfo: Fo result[i] = new ModelLineProjectionData(injectionOffsets, injectionOptions, breakOffsets, breakOffsetsVisibleColumn, wrappedTextIndentLength); } - targetWindow.document.body.removeChild(containerDomNode); + containerDomNode.remove(); return result; } diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index bbbb0dd9d73..971d8ae4011 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -295,9 +295,7 @@ export class VisibleLinesCollection { // Remove from DOM for (let i = 0, len = deleted.length; i < len; i++) { const lineDomNode = deleted[i].getDomNode(); - if (lineDomNode) { - this.domNode.domNode.removeChild(lineDomNode); - } + lineDomNode?.remove(); } } @@ -310,9 +308,7 @@ export class VisibleLinesCollection { // Remove from DOM for (let i = 0, len = deleted.length; i < len; i++) { const lineDomNode = deleted[i].getDomNode(); - if (lineDomNode) { - this.domNode.domNode.removeChild(lineDomNode); - } + lineDomNode?.remove(); } } @@ -481,9 +477,7 @@ class ViewLayerRenderer { private _removeLinesBefore(ctx: IRendererContext, removeCount: number): void { for (let i = 0; i < removeCount; i++) { const lineDomNode = ctx.lines[i].getDomNode(); - if (lineDomNode) { - this.domNode.removeChild(lineDomNode); - } + lineDomNode?.remove(); } ctx.lines.splice(0, removeCount); } @@ -502,9 +496,7 @@ class ViewLayerRenderer { for (let i = 0; i < removeCount; i++) { const lineDomNode = ctx.lines[removeIndex + i].getDomNode(); - if (lineDomNode) { - this.domNode.removeChild(lineDomNode); - } + lineDomNode?.remove(); } ctx.lines.splice(removeIndex, removeCount); } diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index a11435b8e73..bdf8eb77d21 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -121,7 +121,7 @@ export class ViewContentWidgets extends ViewPart { delete this._widgets[widgetId]; const domNode = myWidget.domNode.domNode; - domNode.parentNode!.removeChild(domNode); + domNode.remove(); domNode.removeAttribute('monaco-visible-content-widget'); this.setShouldRender(); diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 48c79a783ea..164b2299b75 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -243,7 +243,7 @@ export class GlyphMarginWidgets extends ViewPart { const domNode = widgetData.domNode.domNode; delete this._widgets[widgetId]; - domNode.parentNode?.removeChild(domNode); + domNode.remove(); this.setShouldRender(); } } diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 37914a70335..a99dac77cde 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -271,12 +271,12 @@ export class ViewZones extends ViewPart { zone.domNode.removeAttribute('monaco-visible-view-zone'); zone.domNode.removeAttribute('monaco-view-zone'); - zone.domNode.domNode.parentNode!.removeChild(zone.domNode.domNode); + zone.domNode.domNode.remove(); if (zone.marginDomNode) { zone.marginDomNode.removeAttribute('monaco-visible-view-zone'); zone.marginDomNode.removeAttribute('monaco-view-zone'); - zone.marginDomNode.domNode.parentNode!.removeChild(zone.marginDomNode.domNode); + zone.marginDomNode.domNode.remove(); } this.setShouldRender(); diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 07753688f7e..f8ed64fba6c 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -1613,7 +1613,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE public setBanner(domNode: HTMLElement | null, domNodeHeight: number): void { if (this._bannerDomNode && this._domElement.contains(this._bannerDomNode)) { - this._domElement.removeChild(this._bannerDomNode); + this._bannerDomNode.remove(); } this._bannerDomNode = domNode; @@ -1648,6 +1648,16 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this.languageConfigurationService, this._themeService, attachedView, + { + batchChanges: (cb) => { + try { + this._beginUpdate(); + return cb(); + } finally { + this._endUpdate(); + } + }, + } ); // Someone might destroy the model from under the editor, so prevent any exceptions by setting a null model @@ -1874,10 +1884,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._domElement.removeAttribute('data-mode-id'); if (removeDomNode && this._domElement.contains(removeDomNode)) { - this._domElement.removeChild(removeDomNode); + removeDomNode.remove(); } if (this._bannerDomNode && this._domElement.contains(this._bannerDomNode)) { - this._domElement.removeChild(this._bannerDomNode); + this._bannerDomNode.remove(); } return model; } diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts index db99842d621..55b5d4a1e54 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts @@ -8,7 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IReader, autorunHandleChanges, derived, derivedOpts, observableFromEvent } from 'vs/base/common/observable'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; -import { obsCodeEditor } from 'vs/editor/browser/observableUtilities'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { OverviewRulerFeature } from 'vs/editor/browser/widget/diffEditor/features/overviewRulerFeature'; @@ -27,18 +27,18 @@ export class DiffEditorEditors extends Disposable { private readonly _onDidContentSizeChange = this._register(new Emitter()); public get onDidContentSizeChange() { return this._onDidContentSizeChange.event; } - public readonly modifiedScrollTop = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop()); - public readonly modifiedScrollHeight = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight()); + public readonly modifiedScrollTop = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop()); + public readonly modifiedScrollHeight = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight()); - public readonly modifiedModel = obsCodeEditor(this.modified).model; + public readonly modifiedModel = observableCodeEditor(this.modified).model; - public readonly modifiedSelections = observableFromEvent(this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []); + public readonly modifiedSelections = observableFromEvent(this, this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []); public readonly modifiedCursor = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.modifiedSelections.read(reader)[0]?.getPosition() ?? new Position(1, 1)); - public readonly originalCursor = observableFromEvent(this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1)); + public readonly originalCursor = observableFromEvent(this, this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1)); - public readonly isOriginalFocused = obsCodeEditor(this.original).isFocused; - public readonly isModifiedFocused = obsCodeEditor(this.modified).isFocused; + public readonly isOriginalFocused = observableCodeEditor(this.original).isFocused; + public readonly isModifiedFocused = observableCodeEditor(this.modified).isFocused; public readonly isFocused = derived(this, reader => this.isOriginalFocused.read(reader) || this.isModifiedFocused.read(reader)); diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts index 23a75bac47d..6a5283f2491 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts @@ -81,7 +81,7 @@ export class DiffEditorViewZones extends Disposable { })); const originalModelTokenizationCompleted = this._diffModel.map(m => - m ? observableFromEvent(m.model.original.onDidChangeTokens, () => m.model.original.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) : undefined + m ? observableFromEvent(this, m.model.original.onDidChangeTokens, () => m.model.original.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) : undefined ).map((m, reader) => m?.read(reader)); const alignments = derived((reader) => { @@ -525,13 +525,15 @@ function computeRangeAlignment( let lastModLineNumber = c.modified.startLineNumber; let lastOrigLineNumber = c.original.startLineNumber; - function emitAlignment(origLineNumberExclusive: number, modLineNumberExclusive: number) { + function emitAlignment(origLineNumberExclusive: number, modLineNumberExclusive: number, forceAlignment = false) { if (origLineNumberExclusive < lastOrigLineNumber || modLineNumberExclusive < lastModLineNumber) { return; } if (first) { first = false; - } else if (origLineNumberExclusive === lastOrigLineNumber || modLineNumberExclusive === lastModLineNumber) { + } else if (!forceAlignment && (origLineNumberExclusive === lastOrigLineNumber || modLineNumberExclusive === lastModLineNumber)) { + // This causes a re-alignment of an already aligned line. + // However, we don't care for the final alignment. return; } const originalRange = new LineRange(lastOrigLineNumber, origLineNumberExclusive); @@ -575,7 +577,7 @@ function computeRangeAlignment( } } - emitAlignment(c.original.endLineNumberExclusive, c.modified.endLineNumberExclusive); + emitAlignment(c.original.endLineNumberExclusive, c.modified.endLineNumberExclusive, true); lastOriginalLineNumber = c.original.endLineNumberExclusive; lastModifiedLineNumber = c.modified.endLineNumberExclusive; diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts index f5bbdb1b43f..b53bb2660e3 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts @@ -16,7 +16,7 @@ export class DiffEditorOptions { private readonly _diffEditorWidth = observableValue(this, 0); - private readonly _screenReaderMode = observableFromEvent(this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); + private readonly _screenReaderMode = observableFromEvent(this, this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); constructor( options: Readonly, diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index c3c798529ca..29942806d0c 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -5,7 +5,7 @@ import { getWindow, h } from 'vs/base/browser/dom'; import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; import { findLast } from 'vs/base/common/arraysFind'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { toDisposable } from 'vs/base/common/lifecycle'; import { IObservable, ITransaction, autorun, autorunWithStore, derived, observableFromEvent, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from 'vs/base/common/observable'; @@ -111,7 +111,7 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { this._contextKeyService.createKey('isInDiffEditor', true); this._domElement.appendChild(this.elements.root); - this._register(toDisposable(() => this._domElement.removeChild(this.elements.root))); + this._register(toDisposable(() => this.elements.root.remove())); this._rootSizeObserver = this._register(new ObservableElementSizeObserver(this.elements.root, options.dimension)); this._rootSizeObserver.setAutomaticLayout(options.automaticLayout ?? false); @@ -326,6 +326,17 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { this._register(autorunWithStore((reader, store) => { store.add(new (readHotReloadableExport(RevertButtonsFeature, reader))(this._editors, this._diffModel, this._options, this)); })); + + this._register(autorunWithStore((reader, store) => { + const model = this._diffModel.read(reader); + if (!model) { return; } + for (const m of [model.model.original, model.model.modified]) { + store.add(m.onWillDispose(e => { + onUnexpectedError(new BugIndicatingError('TextModel got disposed before DiffEditorWidget model got reset')); + this.setModel(null); + })); + } + })); } public getViewWidth(): number { diff --git a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts index e7c76c727cf..010fba5ec96 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts @@ -37,7 +37,7 @@ const width = 35; export class DiffEditorGutter extends Disposable { private readonly _menu = this._register(this._menuService.createMenu(MenuId.DiffEditorHunkToolbar, this._contextKeyService)); - private readonly _actions = observableFromEvent(this._menu.onDidChange, () => this._menu.getActions()); + private readonly _actions = observableFromEvent(this, this._menu.onDidChange, () => this._menu.getActions()); private readonly _hasActions = this._actions.map(a => a.length > 0); private readonly _showSash = derived(this, reader => this._options.renderSideBySide.read(reader) && this._hasActions.read(reader)); diff --git a/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts index 7d0d3a992c1..afb29693f81 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts @@ -26,8 +26,8 @@ export class MovedBlocksLinesFeature extends Disposable { public static readonly movedCodeBlockPadding = 4; private readonly _element: SVGElement; - private readonly _originalScrollTop = observableFromEvent(this._editors.original.onDidScrollChange, () => this._editors.original.getScrollTop()); - private readonly _modifiedScrollTop = observableFromEvent(this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop()); + private readonly _originalScrollTop = observableFromEvent(this, this._editors.original.onDidScrollChange, () => this._editors.original.getScrollTop()); + private readonly _modifiedScrollTop = observableFromEvent(this, this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop()); private readonly _viewZonesChanged = observableSignalFromEvent('onDidChangeViewZones', this._editors.modified.onDidChangeViewZones); public readonly width = observableValue(this, 0); diff --git a/src/vs/editor/browser/widget/diffEditor/registrations.contribution.ts b/src/vs/editor/browser/widget/diffEditor/registrations.contribution.ts index 36bd4d465ff..80b553bcb30 100644 --- a/src/vs/editor/browser/widget/diffEditor/registrations.contribution.ts +++ b/src/vs/editor/browser/widget/diffEditor/registrations.contribution.ts @@ -12,13 +12,13 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; export const diffMoveBorder = registerColor( 'diffEditor.move.border', - { dark: '#8b8b8b9c', light: '#8b8b8b9c', hcDark: '#8b8b8b9c', hcLight: '#8b8b8b9c', }, + '#8b8b8b9c', localize('diffEditor.move.border', 'The border color for text that got moved in the diff editor.') ); export const diffMoveBorderActive = registerColor( 'diffEditor.moveActive.border', - { dark: '#FFA500', light: '#FFA500', hcDark: '#FFA500', hcLight: '#FFA500', }, + '#FFA500', localize('diffEditor.moveActive.border', 'The active border color for text that got moved in the diff editor.') ); diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index 76c37c4185d..99d9580c94a 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -76,14 +76,14 @@ export function applyObservableDecorations(editor: ICodeEditor, decorations: IOb export function appendRemoveOnDispose(parent: HTMLElement, child: HTMLElement) { parent.appendChild(child); return toDisposable(() => { - parent.removeChild(child); + child.remove(); }); } export function prependRemoveOnDispose(parent: HTMLElement, child: HTMLElement) { parent.prepend(child); return toDisposable(() => { - parent.removeChild(child); + child.remove(); }); } diff --git a/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts b/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts index 1c3341a73ef..a301fc6124b 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts @@ -11,12 +11,12 @@ import { LineRange } from 'vs/editor/common/core/lineRange'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; export class EditorGutter extends Disposable { - private readonly scrollTop = observableFromEvent( + private readonly scrollTop = observableFromEvent(this, this._editor.onDidScrollChange, (e) => /** @description editor.onDidScrollChange */ this._editor.getScrollTop() ); private readonly isScrollTopZero = this.scrollTop.map((scrollTop) => /** @description isScrollTopZero */ scrollTop === 0); - private readonly modelAttached = observableFromEvent( + private readonly modelAttached = observableFromEvent(this, this._editor.onDidChangeModel, (e) => /** @description editor.onDidChangeModel */ this._editor.hasModel() ); @@ -136,7 +136,7 @@ export class EditorGutter extends D for (const id of unusedIds) { const view = this.views.get(id)!; view.gutterItemView.dispose(); - this._domNode.removeChild(view.domNode); + view.domNode.remove(); this.views.delete(id); } } diff --git a/src/vs/editor/browser/widget/multiDiffEditor/colors.ts b/src/vs/editor/browser/widget/multiDiffEditor/colors.ts index d58781aabfe..297e5e86465 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/colors.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/colors.ts @@ -14,7 +14,7 @@ export const multiDiffEditorHeaderBackground = registerColor( export const multiDiffEditorBackground = registerColor( 'multiDiffEditor.background', - { dark: 'editorBackground', light: 'editorBackground', hcDark: 'editorBackground', hcLight: 'editorBackground', }, + 'editorBackground', localize('multiDiffEditor.background', 'The background color of the multi file diff editor') ); diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts index c29fc74bddd..eab554dbdfb 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts @@ -73,8 +73,8 @@ export class MultiDiffEditorWidgetImpl extends Disposable { return template; })); - public readonly scrollTop = observableFromEvent(this._scrollableElement.onScroll, () => /** @description scrollTop */ this._scrollableElement.getScrollPosition().scrollTop); - public readonly scrollLeft = observableFromEvent(this._scrollableElement.onScroll, () => /** @description scrollLeft */ this._scrollableElement.getScrollPosition().scrollLeft); + public readonly scrollTop = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollTop */ this._scrollableElement.getScrollPosition().scrollTop); + public readonly scrollLeft = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollLeft */ this._scrollableElement.getScrollPosition().scrollLeft); private readonly _viewItemsInfo = derivedWithStore<{ items: readonly VirtualizedViewItem[]; getItem: (viewModel: DocumentDiffItemViewModel) => VirtualizedViewItem }>(this, (reader, store) => { diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index b0a3c4bba1b..ddf3fd27248 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -706,6 +706,13 @@ export interface IEditorOptions { * Defaults to false. */ peekWidgetDefaultFocus?: 'tree' | 'editor'; + + /** + * Sets a placeholder for the editor. + * If set, the placeholder is shown if the editor is empty. + */ + placeholder?: string | undefined; + /** * Controls whether the definition link opens element in the peek widget. * Defaults to false. @@ -3363,6 +3370,25 @@ class EditorPixelRatio extends ComputedEditorOption { + constructor() { + super(EditorOption.placeholder, 'placeholder', undefined); + } + + public validate(input: any): string | undefined { + if (typeof input === 'undefined') { + return this.defaultValue; + } + if (typeof input === 'string') { + return input; + } + return this.defaultValue; + } +} +//#endregion + //#region quickSuggestions export type QuickSuggestionsValue = 'on' | 'inline' | 'off'; @@ -3421,7 +3447,7 @@ class EditorQuickSuggestions extends BaseEditorOption { + return shiftIndent(config, indentation); + }, + unshiftIndent: (indentation) => { + return unshiftIndent(config, indentation); + }, + }, config.languageConfigurationService); + + if (actualIndentation === null) { + return null; + } + + const currentIndentation = getIndentationAtPosition(model, selection.startLineNumber, selection.startColumn); + if (actualIndentation === config.normalizeIndentation(currentIndentation)) { + return null; + } + return actualIndentation; + } + + private static _getIndentationAndAutoClosingPairEdits(config: CursorConfiguration, model: ITextModel, indentationForSelections: { selection: Selection; indentation: string }[], ch: string, autoClosingPairClose: string | null): EditOperationResult { + const commands: ICommand[] = indentationForSelections.map(({ selection, indentation }) => { + if (autoClosingPairClose !== null) { + // Apply both auto closing pair edits and auto indentation edits + const indentationEdit = this._getEditFromIndentationAndSelection(config, model, indentation, selection, ch, false); + return new TypeWithIndentationAndAutoClosingCommand(indentationEdit, selection, ch, autoClosingPairClose); + } else { + // Apply only auto indentation edits + const indentationEdit = this._getEditFromIndentationAndSelection(config, model, indentation, selection, ch, true); + return typeCommand(indentationEdit.range, indentationEdit.text, false); + } + }); + const editOptions = { shouldPushStackElementBefore: true, shouldPushStackElementAfter: false }; + return new EditOperationResult(EditOperationType.TypingOther, commands, editOptions); + } + + private static _getEditFromIndentationAndSelection(config: CursorConfiguration, model: ITextModel, indentation: string, selection: Selection, ch: string, includeChInEdit: boolean = true): { range: Range; text: string } { + const startLineNumber = selection.startLineNumber; + const firstNonWhitespaceColumn = model.getLineFirstNonWhitespaceColumn(startLineNumber); + let text: string = config.normalizeIndentation(indentation); + if (firstNonWhitespaceColumn !== 0) { + const startLine = model.getLineContent(startLineNumber); + text += startLine.substring(firstNonWhitespaceColumn - 1, selection.startColumn - 1); + } + text += includeChInEdit ? ch : ''; + const range = new Range(startLineNumber, 1, selection.endLineNumber, selection.endColumn); + return { range, text }; + } +} + +export class AutoClosingOvertypeOperation { + + public static getEdits(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult | undefined { + if (isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { + return this._runAutoClosingOvertype(prevEditOperationType, selections, ch); + } + return; + } + + private static _runAutoClosingOvertype(prevEditOperationType: EditOperationType, selections: Selection[], ch: string): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + const position = selection.getPosition(); + const typeSelection = new Range(position.lineNumber, position.column, position.lineNumber, position.column + 1); + commands[i] = new ReplaceCommand(typeSelection, ch); + } + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), + shouldPushStackElementAfter: false + }); + } +} + +export class AutoClosingOvertypeWithInterceptorsOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult | undefined { + if (isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { + // Unfortunately, the close character is at this point "doubled", so we need to delete it... + const commands = selections.map(s => new ReplaceCommand(new Range(s.positionLineNumber, s.positionColumn, s.positionLineNumber, s.positionColumn + 1), '', false)); + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: false + }); + } + return; + } +} + +export class AutoClosingOpenCharTypeOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean, isDoingComposition: boolean): EditOperationResult | undefined { + if (!isDoingComposition) { + const autoClosingPairClose = this.getAutoClosingPairClose(config, model, selections, ch, chIsAlreadyTyped); + if (autoClosingPairClose !== null) { + return this._runAutoClosingOpenCharType(selections, ch, chIsAlreadyTyped, autoClosingPairClose); + } + } + return; + } + + private static _runAutoClosingOpenCharType(selections: Selection[], ch: string, chIsAlreadyTyped: boolean, autoClosingPairClose: string): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + commands[i] = new TypeWithAutoClosingCommand(selection, ch, !chIsAlreadyTyped, autoClosingPairClose); + } + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: false + }); + } + + public static getAutoClosingPairClose(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean): string | null { + for (const selection of selections) { + if (!selection.isEmpty()) { + return null; + } + } + // This method is called both when typing (regularly) and when composition ends + // This means that we need to work with a text buffer where sometimes `ch` is not + // there (it is being typed right now) or with a text buffer where `ch` has already been typed + // + // In order to avoid adding checks for `chIsAlreadyTyped` in all places, we will work + // with two conceptual positions, the position before `ch` and the position after `ch` + // + const positions: { lineNumber: number; beforeColumn: number; afterColumn: number }[] = selections.map((s) => { + const position = s.getPosition(); + if (chIsAlreadyTyped) { + return { lineNumber: position.lineNumber, beforeColumn: position.column - ch.length, afterColumn: position.column }; + } else { + return { lineNumber: position.lineNumber, beforeColumn: position.column, afterColumn: position.column }; + } + }); + // Find the longest auto-closing open pair in case of multiple ending in `ch` + // e.g. when having [f","] and [","], it picks [f","] if the character before is f + const pair = this._findAutoClosingPairOpen(config, model, positions.map(p => new Position(p.lineNumber, p.beforeColumn)), ch); + if (!pair) { + return null; + } + let autoCloseConfig: EditorAutoClosingStrategy; + let shouldAutoCloseBefore: (ch: string) => boolean; + + const chIsQuote = isQuote(ch); + if (chIsQuote) { + autoCloseConfig = config.autoClosingQuotes; + shouldAutoCloseBefore = config.shouldAutoCloseBefore.quote; + } else { + const pairIsForComments = config.blockCommentStartToken ? pair.open.includes(config.blockCommentStartToken) : false; + if (pairIsForComments) { + autoCloseConfig = config.autoClosingComments; + shouldAutoCloseBefore = config.shouldAutoCloseBefore.comment; + } else { + autoCloseConfig = config.autoClosingBrackets; + shouldAutoCloseBefore = config.shouldAutoCloseBefore.bracket; + } + } + if (autoCloseConfig === 'never') { + return null; + } + // Sometimes, it is possible to have two auto-closing pairs that have a containment relationship + // e.g. when having [(,)] and [(*,*)] + // - when typing (, the resulting state is (|) + // - when typing *, the desired resulting state is (*|*), not (*|*)) + const containedPair = this._findContainedAutoClosingPair(config, pair); + const containedPairClose = containedPair ? containedPair.close : ''; + let isContainedPairPresent = true; + + for (const position of positions) { + const { lineNumber, beforeColumn, afterColumn } = position; + const lineText = model.getLineContent(lineNumber); + const lineBefore = lineText.substring(0, beforeColumn - 1); + const lineAfter = lineText.substring(afterColumn - 1); + + if (!lineAfter.startsWith(containedPairClose)) { + isContainedPairPresent = false; + } + // Only consider auto closing the pair if an allowed character follows or if another autoclosed pair closing brace follows + if (lineAfter.length > 0) { + const characterAfter = lineAfter.charAt(0); + const isBeforeCloseBrace = this._isBeforeClosingBrace(config, lineAfter); + if (!isBeforeCloseBrace && !shouldAutoCloseBefore(characterAfter)) { + return null; + } + } + // Do not auto-close ' or " after a word character + if (pair.open.length === 1 && (ch === '\'' || ch === '"') && autoCloseConfig !== 'always') { + const wordSeparators = getMapForWordSeparators(config.wordSeparators, []); + if (lineBefore.length > 0) { + const characterBefore = lineBefore.charCodeAt(lineBefore.length - 1); + if (wordSeparators.get(characterBefore) === WordCharacterClass.Regular) { + return null; + } + } + } + if (!model.tokenization.isCheapToTokenize(lineNumber)) { + // Do not force tokenization + return null; + } + model.tokenization.forceTokenization(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); + const scopedLineTokens = createScopedLineTokens(lineTokens, beforeColumn - 1); + if (!pair.shouldAutoClose(scopedLineTokens, beforeColumn - scopedLineTokens.firstCharOffset)) { + return null; + } + // Typing for example a quote could either start a new string, in which case auto-closing is desirable + // or it could end a previously started string, in which case auto-closing is not desirable + // + // In certain cases, it is really not possible to look at the previous token to determine + // what would happen. That's why we do something really unusual, we pretend to type a different + // character and ask the tokenizer what the outcome of doing that is: after typing a neutral + // character, are we in a string (i.e. the quote would most likely end a string) or not? + // + const neutralCharacter = pair.findNeutralCharacter(); + if (neutralCharacter) { + const tokenType = model.tokenization.getTokenTypeIfInsertingCharacter(lineNumber, beforeColumn, neutralCharacter); + if (!pair.isOK(tokenType)) { + return null; + } + } + } + if (isContainedPairPresent) { + return pair.close.substring(0, pair.close.length - containedPairClose.length); + } else { + return pair.close; + } + } + + /** + * Find another auto-closing pair that is contained by the one passed in. + * + * e.g. when having [(,)] and [(*,*)] as auto-closing pairs + * this method will find [(,)] as a containment pair for [(*,*)] + */ + private static _findContainedAutoClosingPair(config: CursorConfiguration, pair: StandardAutoClosingPairConditional): StandardAutoClosingPairConditional | null { + if (pair.open.length <= 1) { + return null; + } + const lastChar = pair.close.charAt(pair.close.length - 1); + // get candidates with the same last character as close + const candidates = config.autoClosingPairs.autoClosingPairsCloseByEnd.get(lastChar) || []; + let result: StandardAutoClosingPairConditional | null = null; + for (const candidate of candidates) { + if (candidate.open !== pair.open && pair.open.includes(candidate.open) && pair.close.endsWith(candidate.close)) { + if (!result || candidate.open.length > result.open.length) { + result = candidate; + } + } + } + return result; + } + + /** + * Determine if typing `ch` at all `positions` in the `model` results in an + * auto closing open sequence being typed. + * + * Auto closing open sequences can consist of multiple characters, which + * can lead to ambiguities. In such a case, the longest auto-closing open + * sequence is returned. + */ + private static _findAutoClosingPairOpen(config: CursorConfiguration, model: ITextModel, positions: Position[], ch: string): StandardAutoClosingPairConditional | null { + const candidates = config.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch); + if (!candidates) { + return null; + } + // Determine which auto-closing pair it is + let result: StandardAutoClosingPairConditional | null = null; + for (const candidate of candidates) { + if (result === null || candidate.open.length > result.open.length) { + let candidateIsMatch = true; + for (const position of positions) { + const relevantText = model.getValueInRange(new Range(position.lineNumber, position.column - candidate.open.length + 1, position.lineNumber, position.column)); + if (relevantText + ch !== candidate.open) { + candidateIsMatch = false; + break; + } + } + if (candidateIsMatch) { + result = candidate; + } + } + } + return result; + } + + private static _isBeforeClosingBrace(config: CursorConfiguration, lineAfter: string) { + // If the start of lineAfter can be interpretted as both a starting or ending brace, default to returning false + const nextChar = lineAfter.charAt(0); + const potentialStartingBraces = config.autoClosingPairs.autoClosingPairsOpenByStart.get(nextChar) || []; + const potentialClosingBraces = config.autoClosingPairs.autoClosingPairsCloseByStart.get(nextChar) || []; + + const isBeforeStartingBrace = potentialStartingBraces.some(x => lineAfter.startsWith(x.open)); + const isBeforeClosingBrace = potentialClosingBraces.some(x => lineAfter.startsWith(x.close)); + + return !isBeforeStartingBrace && isBeforeClosingBrace; + } +} + +export class SurroundSelectionOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult | undefined { + if (!isDoingComposition && this._isSurroundSelectionType(config, model, selections, ch)) { + return this._runSurroundSelectionType(config, selections, ch); + } + return; + } + + private static _runSurroundSelectionType(config: CursorConfiguration, selections: Selection[], ch: string): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + const closeCharacter = config.surroundingPairs[ch]; + commands[i] = new SurroundSelectionCommand(selection, ch, closeCharacter); + } + return new EditOperationResult(EditOperationType.Other, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: true + }); + } + + private static _isSurroundSelectionType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): boolean { + if (!shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) { + return false; + } + const isTypingAQuoteCharacter = isQuote(ch); + for (const selection of selections) { + if (selection.isEmpty()) { + return false; + } + let selectionContainsOnlyWhitespace = true; + for (let lineNumber = selection.startLineNumber; lineNumber <= selection.endLineNumber; lineNumber++) { + const lineText = model.getLineContent(lineNumber); + const startIndex = (lineNumber === selection.startLineNumber ? selection.startColumn - 1 : 0); + const endIndex = (lineNumber === selection.endLineNumber ? selection.endColumn - 1 : lineText.length); + const selectedText = lineText.substring(startIndex, endIndex); + if (/[^ \t]/.test(selectedText)) { + // this selected text contains something other than whitespace + selectionContainsOnlyWhitespace = false; + break; + } + } + if (selectionContainsOnlyWhitespace) { + return false; + } + if (isTypingAQuoteCharacter && selection.startLineNumber === selection.endLineNumber && selection.startColumn + 1 === selection.endColumn) { + const selectionText = model.getValueInRange(selection); + if (isQuote(selectionText)) { + // Typing a quote character on top of another quote character + // => disable surround selection type + return false; + } + } + } + return true; + } +} + +export class InterceptorElectricCharOperation { + + public static getEdits(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult | undefined { + // Electric characters make sense only when dealing with a single cursor, + // as multiple cursors typing brackets for example would interfer with bracket matching + if (!isDoingComposition && this._isTypeInterceptorElectricChar(config, model, selections)) { + const r = this._typeInterceptorElectricChar(prevEditOperationType, config, model, selections[0], ch); + if (r) { + return r; + } + } + return; + } + + private static _isTypeInterceptorElectricChar(config: CursorConfiguration, model: ITextModel, selections: Selection[]) { + if (selections.length === 1 && model.tokenization.isCheapToTokenize(selections[0].getEndPosition().lineNumber)) { + return true; + } + return false; + } + + private static _typeInterceptorElectricChar(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selection: Selection, ch: string): EditOperationResult | null { + if (!config.electricChars.hasOwnProperty(ch) || !selection.isEmpty()) { + return null; + } + const position = selection.getPosition(); + model.tokenization.forceTokenization(position.lineNumber); + const lineTokens = model.tokenization.getLineTokens(position.lineNumber); + let electricAction: IElectricAction | null; + try { + electricAction = config.onElectricCharacter(ch, lineTokens, position.column); + } catch (e) { + onUnexpectedError(e); + return null; + } + if (!electricAction) { + return null; + } + if (electricAction.matchOpenBracket) { + const endColumn = (lineTokens.getLineContent() + ch).lastIndexOf(electricAction.matchOpenBracket) + 1; + const match = model.bracketPairs.findMatchingBracketUp(electricAction.matchOpenBracket, { + lineNumber: position.lineNumber, + column: endColumn + }, 500 /* give at most 500ms to compute */); + if (match) { + if (match.startLineNumber === position.lineNumber) { + // matched something on the same line => no change in indentation + return null; + } + const matchLine = model.getLineContent(match.startLineNumber); + const matchLineIndentation = strings.getLeadingWhitespace(matchLine); + const newIndentation = config.normalizeIndentation(matchLineIndentation); + const lineText = model.getLineContent(position.lineNumber); + const lineFirstNonBlankColumn = model.getLineFirstNonWhitespaceColumn(position.lineNumber) || position.column; + const prefix = lineText.substring(lineFirstNonBlankColumn - 1, position.column - 1); + const typeText = newIndentation + prefix + ch; + const typeSelection = new Range(position.lineNumber, 1, position.lineNumber, position.column); + const command = new ReplaceCommand(typeSelection, typeText); + return new EditOperationResult(getTypingOperation(typeText, prevEditOperationType), [command], { + shouldPushStackElementBefore: false, + shouldPushStackElementAfter: true + }); + } + } + return null; + } +} + +export class SimpleCharacterTypeOperation { + + public static getEdits(prevEditOperationType: EditOperationType, selections: Selection[], ch: string): 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 opType = getTypingOperation(ch, prevEditOperationType); + return new EditOperationResult(opType, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), + shouldPushStackElementAfter: false + }); + } +} + +export class EnterOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult | undefined { + if (!isDoingComposition && ch === '\n') { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + commands[i] = this._enter(config, model, false, selections[i]); + } + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: false, + }); + } + return; + } + + private static _enter(config: CursorConfiguration, model: ITextModel, keepPosition: boolean, range: Range): ICommand { + if (config.autoIndent === EditorAutoIndentStrategy.None) { + return typeCommand(range, '\n', keepPosition); + } + if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber) || config.autoIndent === EditorAutoIndentStrategy.Keep) { + const lineText = model.getLineContent(range.startLineNumber); + const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); + return typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); + } + const r = getEnterAction(config.autoIndent, model, range, config.languageConfigurationService); + if (r) { + if (r.indentAction === IndentAction.None) { + // Nothing special + return typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); + + } else if (r.indentAction === IndentAction.Indent) { + // Indent once + return typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); + + } else if (r.indentAction === IndentAction.IndentOutdent) { + // Ultra special + const normalIndent = config.normalizeIndentation(r.indentation); + const increasedIndent = config.normalizeIndentation(r.indentation + r.appendText); + const typeText = '\n' + increasedIndent + '\n' + normalIndent; + if (keepPosition) { + return new ReplaceCommandWithoutChangingPosition(range, typeText, true); + } else { + return new ReplaceCommandWithOffsetCursorState(range, typeText, -1, increasedIndent.length - normalIndent.length, true); + } + } else if (r.indentAction === IndentAction.Outdent) { + const actualIndentation = unshiftIndent(config, r.indentation); + return typeCommand(range, '\n' + config.normalizeIndentation(actualIndentation + r.appendText), keepPosition); + } + } + + const lineText = model.getLineContent(range.startLineNumber); + const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); + + if (config.autoIndent >= EditorAutoIndentStrategy.Full) { + const ir = getIndentForEnter(config.autoIndent, model, range, { + unshiftIndent: (indent) => { + return unshiftIndent(config, indent); + }, + shiftIndent: (indent) => { + return shiftIndent(config, indent); + }, + normalizeIndentation: (indent) => { + return config.normalizeIndentation(indent); + } + }, config.languageConfigurationService); + + if (ir) { + let oldEndViewColumn = config.visibleColumnFromColumn(model, range.getEndPosition()); + const oldEndColumn = range.endColumn; + const newLineContent = model.getLineContent(range.endLineNumber); + const firstNonWhitespace = strings.firstNonWhitespaceIndex(newLineContent); + if (firstNonWhitespace >= 0) { + range = range.setEndPosition(range.endLineNumber, Math.max(range.endColumn, firstNonWhitespace + 1)); + } else { + range = range.setEndPosition(range.endLineNumber, model.getLineMaxColumn(range.endLineNumber)); + } + if (keepPosition) { + return new ReplaceCommandWithoutChangingPosition(range, '\n' + config.normalizeIndentation(ir.afterEnter), true); + } else { + let offset = 0; + if (oldEndColumn <= firstNonWhitespace + 1) { + if (!config.insertSpaces) { + oldEndViewColumn = Math.ceil(oldEndViewColumn / config.indentSize); + } + offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0); + } + return new ReplaceCommandWithOffsetCursorState(range, '\n' + config.normalizeIndentation(ir.afterEnter), 0, offset, true); + } + } + } + return typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); + } + + + public static lineInsertBefore(config: CursorConfiguration, model: ITextModel | null, selections: Selection[] | null): ICommand[] { + if (model === null || selections === null) { + return []; + } + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + let lineNumber = selections[i].positionLineNumber; + if (lineNumber === 1) { + commands[i] = new ReplaceCommandWithoutChangingPosition(new Range(1, 1, 1, 1), '\n'); + } else { + lineNumber--; + const column = model.getLineMaxColumn(lineNumber); + + commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column)); + } + } + return commands; + } + + public static lineInsertAfter(config: CursorConfiguration, model: ITextModel | null, selections: Selection[] | null): ICommand[] { + if (model === null || selections === null) { + return []; + } + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const lineNumber = selections[i].positionLineNumber; + const column = model.getLineMaxColumn(lineNumber); + commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column)); + } + return commands; + } + + public static lineBreakInsert(config: CursorConfiguration, model: ITextModel, selections: Selection[]): ICommand[] { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + commands[i] = this._enter(config, model, true, selections[i]); + } + return commands; + } +} + +export class PasteOperation { + + public static getEdits(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string, pasteOnNewLine: boolean, multicursorText: string[]) { + const distributedPaste = this._distributePasteToCursors(config, selections, text, pasteOnNewLine, multicursorText); + if (distributedPaste) { + selections = selections.sort(Range.compareRangesUsingStarts); + return this._distributedPaste(config, model, selections, distributedPaste); + } else { + return this._simplePaste(config, model, selections, text, pasteOnNewLine); + } + } + + private static _distributePasteToCursors(config: CursorConfiguration, selections: Selection[], text: string, pasteOnNewLine: boolean, multicursorText: string[]): string[] | null { + if (pasteOnNewLine) { + return null; + } + if (selections.length === 1) { + return null; + } + if (multicursorText && multicursorText.length === selections.length) { + return multicursorText; + } + if (config.multiCursorPaste === 'spread') { + // Try to spread the pasted text in case the line count matches the cursor count + // Remove trailing \n if present + if (text.charCodeAt(text.length - 1) === CharCode.LineFeed) { + text = text.substring(0, text.length - 1); + } + // Remove trailing \r if present + if (text.charCodeAt(text.length - 1) === CharCode.CarriageReturn) { + text = text.substring(0, text.length - 1); + } + const lines = strings.splitLines(text); + if (lines.length === selections.length) { + return lines; + } + } + return null; + } + + 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]); + } + return new EditOperationResult(EditOperationType.Other, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: true + }); + } + + private static _simplePaste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string, pasteOnNewLine: boolean): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + const position = selection.getPosition(); + if (pasteOnNewLine && !selection.isEmpty()) { + pasteOnNewLine = false; + } + if (pasteOnNewLine && text.indexOf('\n') !== text.length - 1) { + pasteOnNewLine = false; + } + if (pasteOnNewLine) { + // Paste entire line at the beginning of line + 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); + } + } + return new EditOperationResult(EditOperationType.Other, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: true + }); + } +} + +export class CompositionOperation { + + public static getEdits(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number) { + const commands = selections.map(selection => this._compositionType(model, selection, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta)); + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), + shouldPushStackElementAfter: false + }); + } + + private static _compositionType(model: ITextModel, selection: Selection, text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): ICommand | null { + if (!selection.isEmpty()) { + // looks like https://github.com/microsoft/vscode/issues/2773 + // where a cursor operation occurred before a canceled composition + // => ignore composition + return null; + } + const pos = selection.getPosition(); + const startColumn = Math.max(1, pos.column - replacePrevCharCnt); + const endColumn = Math.min(model.getLineMaxColumn(pos.lineNumber), pos.column + replaceNextCharCnt); + const range = new Range(pos.lineNumber, startColumn, pos.lineNumber, endColumn); + const oldText = model.getValueInRange(range); + if (oldText === text && positionDelta === 0) { + // => ignore composition that doesn't do anything + return null; + } + return new ReplaceCommandWithOffsetCursorState(range, text, 0, positionDelta); + } +} + +export class TypeWithoutInterceptorsOperation { + + public static getEdits(prevEditOperationType: EditOperationType, selections: Selection[], str: string): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + commands[i] = new ReplaceCommand(selections[i], str); + } + const opType = getTypingOperation(str, prevEditOperationType); + return new EditOperationResult(opType, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), + shouldPushStackElementAfter: false + }); + } +} + +export class TabOperation { + + public static getCommands(config: CursorConfiguration, model: ITextModel, selections: Selection[]) { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + if (selection.isEmpty()) { + const lineText = model.getLineContent(selection.startLineNumber); + if (/^\s*$/.test(lineText) && model.tokenization.isCheapToTokenize(selection.startLineNumber)) { + let goodIndent = this._goodIndentForLine(config, model, selection.startLineNumber); + goodIndent = goodIndent || '\t'; + const possibleTypeText = config.normalizeIndentation(goodIndent); + if (!lineText.startsWith(possibleTypeText)) { + commands[i] = new ReplaceCommand(new Range(selection.startLineNumber, 1, selection.startLineNumber, lineText.length + 1), possibleTypeText, true); + continue; + } + } + commands[i] = this._replaceJumpToNextIndent(config, model, selection, true); + } else { + if (selection.startLineNumber === selection.endLineNumber) { + const lineMaxColumn = model.getLineMaxColumn(selection.startLineNumber); + if (selection.startColumn !== 1 || selection.endColumn !== lineMaxColumn) { + // This is a single line selection that is not the entire line + commands[i] = this._replaceJumpToNextIndent(config, model, selection, false); + continue; + } + } + commands[i] = new ShiftCommand(selection, { + isUnshift: false, + tabSize: config.tabSize, + indentSize: config.indentSize, + insertSpaces: config.insertSpaces, + useTabStops: config.useTabStops, + autoIndent: config.autoIndent + }, config.languageConfigurationService); + } + } + return commands; + } + + private static _goodIndentForLine(config: CursorConfiguration, model: ITextModel, lineNumber: number): string | null { + let action: IndentAction | EnterAction | null = null; + let indentation: string = ''; + const expectedIndentAction = getInheritIndentForLine(config.autoIndent, model, lineNumber, false, config.languageConfigurationService); + if (expectedIndentAction) { + action = expectedIndentAction.action; + indentation = expectedIndentAction.indentation; + } else if (lineNumber > 1) { + let lastLineNumber: number; + for (lastLineNumber = lineNumber - 1; lastLineNumber >= 1; lastLineNumber--) { + const lineText = model.getLineContent(lastLineNumber); + const nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineText); + if (nonWhitespaceIdx >= 0) { + break; + } + } + if (lastLineNumber < 1) { + // No previous line with content found + return null; + } + const maxColumn = model.getLineMaxColumn(lastLineNumber); + const expectedEnterAction = getEnterAction(config.autoIndent, model, new Range(lastLineNumber, maxColumn, lastLineNumber, maxColumn), config.languageConfigurationService); + if (expectedEnterAction) { + indentation = expectedEnterAction.indentation + expectedEnterAction.appendText; + } + } + if (action) { + if (action === IndentAction.Indent) { + indentation = shiftIndent(config, indentation); + } + if (action === IndentAction.Outdent) { + indentation = unshiftIndent(config, indentation); + } + indentation = config.normalizeIndentation(indentation); + } + if (!indentation) { + return null; + } + return indentation; + } + + private static _replaceJumpToNextIndent(config: CursorConfiguration, model: ICursorSimpleModel, selection: Selection, insertsAutoWhitespace: boolean): ReplaceCommand { + let typeText = ''; + const position = selection.getStartPosition(); + if (config.insertSpaces) { + const visibleColumnFromColumn = config.visibleColumnFromColumn(model, position); + const indentSize = config.indentSize; + const spacesCnt = indentSize - (visibleColumnFromColumn % indentSize); + for (let i = 0; i < spacesCnt; i++) { + typeText += ' '; + } + } else { + typeText = '\t'; + } + return new ReplaceCommand(selection, typeText, insertsAutoWhitespace); + } +} + +export class BaseTypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorState { + + private readonly _openCharacter: string; + private readonly _closeCharacter: string; + public closeCharacterRange: Range | null; + public enclosingRange: Range | null; + + constructor(selection: Selection, text: string, lineNumberDeltaOffset: number, columnDeltaOffset: number, openCharacter: string, closeCharacter: string) { + super(selection, text, lineNumberDeltaOffset, columnDeltaOffset); + this._openCharacter = openCharacter; + this._closeCharacter = closeCharacter; + this.closeCharacterRange = null; + this.enclosingRange = null; + } + + protected _computeCursorStateWithRange(model: ITextModel, range: Range, helper: ICursorStateComputerData): Selection { + this.closeCharacterRange = new Range(range.startLineNumber, range.endColumn - this._closeCharacter.length, range.endLineNumber, range.endColumn); + this.enclosingRange = new Range(range.startLineNumber, range.endColumn - this._openCharacter.length - this._closeCharacter.length, range.endLineNumber, range.endColumn); + return super.computeCursorState(model, helper); + } +} + +class TypeWithAutoClosingCommand extends BaseTypeWithAutoClosingCommand { + + constructor(selection: Selection, openCharacter: string, insertOpenCharacter: boolean, closeCharacter: string) { + const text = (insertOpenCharacter ? openCharacter : '') + closeCharacter; + const lineNumberDeltaOffset = 0; + const columnDeltaOffset = -closeCharacter.length; + super(selection, text, lineNumberDeltaOffset, columnDeltaOffset, openCharacter, closeCharacter); + } + + public override computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { + const inverseEditOperations = helper.getInverseEditOperations(); + const range = inverseEditOperations[0].range; + return this._computeCursorStateWithRange(model, range, helper); + } +} + +class TypeWithIndentationAndAutoClosingCommand extends BaseTypeWithAutoClosingCommand { + + private readonly _autoIndentationEdit: { range: Range; text: string }; + private readonly _autoClosingEdit: { range: Range; text: string }; + + constructor(autoIndentationEdit: { range: Range; text: string }, selection: Selection, openCharacter: string, closeCharacter: string) { + const text = openCharacter + closeCharacter; + const lineNumberDeltaOffset = 0; + const columnDeltaOffset = openCharacter.length; + super(selection, text, lineNumberDeltaOffset, columnDeltaOffset, openCharacter, closeCharacter); + this._autoIndentationEdit = autoIndentationEdit; + this._autoClosingEdit = { range: selection, text }; + } + + public override getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { + builder.addTrackedEditOperation(this._autoIndentationEdit.range, this._autoIndentationEdit.text); + builder.addTrackedEditOperation(this._autoClosingEdit.range, this._autoClosingEdit.text); + } + + public override computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { + const inverseEditOperations = helper.getInverseEditOperations(); + if (inverseEditOperations.length !== 2) { + throw new Error('There should be two inverse edit operations!'); + } + const range1 = inverseEditOperations[0].range; + const range2 = inverseEditOperations[1].range; + const range = range1.plusRange(range2); + return this._computeCursorStateWithRange(model, range, helper); + } +} + +function getTypingOperation(typedText: string, previousTypingOperation: EditOperationType): EditOperationType { + if (typedText === ' ') { + return previousTypingOperation === EditOperationType.TypingFirstSpace + || previousTypingOperation === EditOperationType.TypingConsecutiveSpace + ? EditOperationType.TypingConsecutiveSpace + : EditOperationType.TypingFirstSpace; + } + + return EditOperationType.TypingOther; +} + +function shouldPushStackElementBetween(previousTypingOperation: EditOperationType, typingOperation: EditOperationType): boolean { + if (isTypingOperation(previousTypingOperation) && !isTypingOperation(typingOperation)) { + // Always set an undo stop before non-type operations + return true; + } + if (previousTypingOperation === EditOperationType.TypingFirstSpace) { + // `abc |d`: No undo stop + // `abc |d`: Undo stop + return false; + } + // Insert undo stop between different operation types + return normalizeOperationType(previousTypingOperation) !== normalizeOperationType(typingOperation); +} + +function normalizeOperationType(type: EditOperationType): EditOperationType | 'space' { + return (type === EditOperationType.TypingConsecutiveSpace || type === EditOperationType.TypingFirstSpace) + ? 'space' + : type; +} + +function isTypingOperation(type: EditOperationType): boolean { + return type === EditOperationType.TypingOther + || type === EditOperationType.TypingFirstSpace + || type === EditOperationType.TypingConsecutiveSpace; +} + +function isAutoClosingOvertype(config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): boolean { + if (config.autoClosingOvertype === 'never') { + return false; + } + if (!config.autoClosingPairs.autoClosingPairsCloseSingleChar.has(ch)) { + return false; + } + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + if (!selection.isEmpty()) { + return false; + } + const position = selection.getPosition(); + const lineText = model.getLineContent(position.lineNumber); + const afterCharacter = lineText.charAt(position.column - 1); + if (afterCharacter !== ch) { + return false; + } + // Do not over-type quotes after a backslash + const chIsQuote = isQuote(ch); + const beforeCharacter = position.column > 2 ? lineText.charCodeAt(position.column - 2) : CharCode.Null; + if (beforeCharacter === CharCode.Backslash && chIsQuote) { + return false; + } + // Must over-type a closing character typed by the editor + if (config.autoClosingOvertype === 'auto') { + let found = false; + for (let j = 0, lenJ = autoClosedCharacters.length; j < lenJ; j++) { + const autoClosedCharacter = autoClosedCharacters[j]; + if (position.lineNumber === autoClosedCharacter.startLineNumber && position.column === autoClosedCharacter.startColumn) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + } + return true; +} + +function typeCommand(range: Range, text: string, keepPosition: boolean): ICommand { + if (keepPosition) { + return new ReplaceCommandWithoutChangingPosition(range, text, true); + } else { + return new ReplaceCommand(range, text, true); + } +} + +export function shiftIndent(config: CursorConfiguration, indentation: string, count?: number): string { + count = count || 1; + return ShiftCommand.shiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces); +} + +export function unshiftIndent(config: CursorConfiguration, indentation: string, count?: number): string { + count = count || 1; + return ShiftCommand.unshiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces); +} + +export function shouldSurroundChar(config: CursorConfiguration, ch: string): boolean { + if (isQuote(ch)) { + return (config.autoSurround === 'quotes' || config.autoSurround === 'languageDefined'); + } else { + // Character is a bracket + return (config.autoSurround === 'brackets' || config.autoSurround === 'languageDefined'); + } +} diff --git a/src/vs/editor/common/cursor/cursorTypeOperations.ts b/src/vs/editor/common/cursor/cursorTypeOperations.ts index ffa80cbb63c..b4c65156f85 100644 --- a/src/vs/editor/common/cursor/cursorTypeOperations.ts +++ b/src/vs/editor/common/cursor/cursorTypeOperations.ts @@ -3,26 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CharCode } from 'vs/base/common/charCode'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import * as strings from 'vs/base/common/strings'; -import { ReplaceCommand, ReplaceCommandWithOffsetCursorState, ReplaceCommandWithoutChangingPosition, ReplaceCommandThatPreservesSelection } from 'vs/editor/common/commands/replaceCommand'; import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; -import { CompositionSurroundSelectionCommand, SurroundSelectionCommand } from 'vs/editor/common/commands/surroundSelectionCommand'; +import { CompositionSurroundSelectionCommand } from 'vs/editor/common/commands/surroundSelectionCommand'; import { CursorConfiguration, EditOperationResult, EditOperationType, ICursorSimpleModel, isQuote } from 'vs/editor/common/cursorCommon'; -import { WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/core/wordCharacterClassifier'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { Position } from 'vs/editor/common/core/position'; -import { ICommand, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; +import { ICommand } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { EnterAction, IndentAction, StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; -import { getIndentationAtPosition } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { IElectricAction } from 'vs/editor/common/languages/supports/electricCharacter'; -import { EditorAutoClosingStrategy, EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; -import { createScopedLineTokens } from 'vs/editor/common/languages/supports'; -import { getIndentActionForType, getIndentForEnter, getInheritIndentForLine } from 'vs/editor/common/languages/autoIndent'; -import { getEnterAction } from 'vs/editor/common/languages/enterAction'; +import { AutoClosingOpenCharTypeOperation, AutoClosingOvertypeOperation, AutoClosingOvertypeWithInterceptorsOperation, AutoIndentOperation, CompositionOperation, EnterOperation, InterceptorElectricCharOperation, PasteOperation, shiftIndent, shouldSurroundChar, SimpleCharacterTypeOperation, SurroundSelectionOperation, TabOperation, TypeWithoutInterceptorsOperation, unshiftIndent } from 'vs/editor/common/cursor/cursorTypeEditOperations'; export class TypeOperations { @@ -61,777 +50,23 @@ export class TypeOperations { } public static shiftIndent(config: CursorConfiguration, indentation: string, count?: number): string { - count = count || 1; - return ShiftCommand.shiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces); + return shiftIndent(config, indentation, count); } public static unshiftIndent(config: CursorConfiguration, indentation: string, count?: number): string { - count = count || 1; - return ShiftCommand.unshiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces); - } - - 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]); - } - return new EditOperationResult(EditOperationType.Other, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: true - }); - } - - private static _simplePaste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string, pasteOnNewLine: boolean): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - const position = selection.getPosition(); - - if (pasteOnNewLine && !selection.isEmpty()) { - pasteOnNewLine = false; - } - if (pasteOnNewLine && text.indexOf('\n') !== text.length - 1) { - pasteOnNewLine = false; - } - - if (pasteOnNewLine) { - // Paste entire line at the beginning of line - 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); - } - } - return new EditOperationResult(EditOperationType.Other, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: true - }); - } - - private static _distributePasteToCursors(config: CursorConfiguration, selections: Selection[], text: string, pasteOnNewLine: boolean, multicursorText: string[]): string[] | null { - if (pasteOnNewLine) { - return null; - } - - if (selections.length === 1) { - return null; - } - - if (multicursorText && multicursorText.length === selections.length) { - return multicursorText; - } - - if (config.multiCursorPaste === 'spread') { - // Try to spread the pasted text in case the line count matches the cursor count - // Remove trailing \n if present - if (text.charCodeAt(text.length - 1) === CharCode.LineFeed) { - text = text.substr(0, text.length - 1); - } - // Remove trailing \r if present - if (text.charCodeAt(text.length - 1) === CharCode.CarriageReturn) { - text = text.substr(0, text.length - 1); - } - const lines = strings.splitLines(text); - if (lines.length === selections.length) { - return lines; - } - } - - return null; + return unshiftIndent(config, indentation, count); } public static paste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string, pasteOnNewLine: boolean, multicursorText: string[]): EditOperationResult { - const distributedPaste = this._distributePasteToCursors(config, selections, text, pasteOnNewLine, multicursorText); - - if (distributedPaste) { - selections = selections.sort(Range.compareRangesUsingStarts); - return this._distributedPaste(config, model, selections, distributedPaste); - } else { - return this._simplePaste(config, model, selections, text, pasteOnNewLine); - } - } - - private static _goodIndentForLine(config: CursorConfiguration, model: ITextModel, lineNumber: number): string | null { - let action: IndentAction | EnterAction | null = null; - let indentation: string = ''; - - const expectedIndentAction = getInheritIndentForLine(config.autoIndent, model, lineNumber, false, config.languageConfigurationService); - if (expectedIndentAction) { - action = expectedIndentAction.action; - indentation = expectedIndentAction.indentation; - } else if (lineNumber > 1) { - let lastLineNumber: number; - for (lastLineNumber = lineNumber - 1; lastLineNumber >= 1; lastLineNumber--) { - const lineText = model.getLineContent(lastLineNumber); - const nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineText); - if (nonWhitespaceIdx >= 0) { - break; - } - } - - if (lastLineNumber < 1) { - // No previous line with content found - return null; - } - - const maxColumn = model.getLineMaxColumn(lastLineNumber); - const expectedEnterAction = getEnterAction(config.autoIndent, model, new Range(lastLineNumber, maxColumn, lastLineNumber, maxColumn), config.languageConfigurationService); - if (expectedEnterAction) { - indentation = expectedEnterAction.indentation + expectedEnterAction.appendText; - } - } - - if (action) { - if (action === IndentAction.Indent) { - indentation = TypeOperations.shiftIndent(config, indentation); - } - - if (action === IndentAction.Outdent) { - indentation = TypeOperations.unshiftIndent(config, indentation); - } - - indentation = config.normalizeIndentation(indentation); - } - - if (!indentation) { - return null; - } - - return indentation; - } - - private static _replaceJumpToNextIndent(config: CursorConfiguration, model: ICursorSimpleModel, selection: Selection, insertsAutoWhitespace: boolean): ReplaceCommand { - let typeText = ''; - - const position = selection.getStartPosition(); - if (config.insertSpaces) { - const visibleColumnFromColumn = config.visibleColumnFromColumn(model, position); - const indentSize = config.indentSize; - const spacesCnt = indentSize - (visibleColumnFromColumn % indentSize); - for (let i = 0; i < spacesCnt; i++) { - typeText += ' '; - } - } else { - typeText = '\t'; - } - - return new ReplaceCommand(selection, typeText, insertsAutoWhitespace); + return PasteOperation.getEdits(config, model, selections, text, pasteOnNewLine, multicursorText); } public static tab(config: CursorConfiguration, model: ITextModel, selections: Selection[]): ICommand[] { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - - if (selection.isEmpty()) { - - const lineText = model.getLineContent(selection.startLineNumber); - - if (/^\s*$/.test(lineText) && model.tokenization.isCheapToTokenize(selection.startLineNumber)) { - let goodIndent = this._goodIndentForLine(config, model, selection.startLineNumber); - goodIndent = goodIndent || '\t'; - const possibleTypeText = config.normalizeIndentation(goodIndent); - if (!lineText.startsWith(possibleTypeText)) { - commands[i] = new ReplaceCommand(new Range(selection.startLineNumber, 1, selection.startLineNumber, lineText.length + 1), possibleTypeText, true); - continue; - } - } - - commands[i] = this._replaceJumpToNextIndent(config, model, selection, true); - } else { - if (selection.startLineNumber === selection.endLineNumber) { - const lineMaxColumn = model.getLineMaxColumn(selection.startLineNumber); - if (selection.startColumn !== 1 || selection.endColumn !== lineMaxColumn) { - // This is a single line selection that is not the entire line - commands[i] = this._replaceJumpToNextIndent(config, model, selection, false); - continue; - } - } - - commands[i] = new ShiftCommand(selection, { - isUnshift: false, - tabSize: config.tabSize, - indentSize: config.indentSize, - insertSpaces: config.insertSpaces, - useTabStops: config.useTabStops, - autoIndent: config.autoIndent - }, config.languageConfigurationService); - } - } - return commands; + return TabOperation.getCommands(config, model, selections); } public static compositionType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): EditOperationResult { - const commands = selections.map(selection => this._compositionType(model, selection, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta)); - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), - shouldPushStackElementAfter: false - }); - } - - private static _compositionType(model: ITextModel, selection: Selection, text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): ICommand | null { - if (!selection.isEmpty()) { - // looks like https://github.com/microsoft/vscode/issues/2773 - // where a cursor operation occurred before a canceled composition - // => ignore composition - return null; - } - const pos = selection.getPosition(); - const startColumn = Math.max(1, pos.column - replacePrevCharCnt); - const endColumn = Math.min(model.getLineMaxColumn(pos.lineNumber), pos.column + replaceNextCharCnt); - const range = new Range(pos.lineNumber, startColumn, pos.lineNumber, endColumn); - const oldText = model.getValueInRange(range); - if (oldText === text && positionDelta === 0) { - // => ignore composition that doesn't do anything - return null; - } - return new ReplaceCommandWithOffsetCursorState(range, text, 0, positionDelta); - } - - private static _typeCommand(range: Range, text: string, keepPosition: boolean): ICommand { - if (keepPosition) { - return new ReplaceCommandWithoutChangingPosition(range, text, true); - } else { - return new ReplaceCommand(range, text, true); - } - } - - private static _enter(config: CursorConfiguration, model: ITextModel, keepPosition: boolean, range: Range): ICommand { - if (config.autoIndent === EditorAutoIndentStrategy.None) { - return TypeOperations._typeCommand(range, '\n', keepPosition); - } - if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber) || config.autoIndent === EditorAutoIndentStrategy.Keep) { - const lineText = model.getLineContent(range.startLineNumber); - const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); - } - - const r = getEnterAction(config.autoIndent, model, range, config.languageConfigurationService); - if (r) { - if (r.indentAction === IndentAction.None) { - // Nothing special - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); - - } else if (r.indentAction === IndentAction.Indent) { - // Indent once - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); - - } else if (r.indentAction === IndentAction.IndentOutdent) { - // Ultra special - const normalIndent = config.normalizeIndentation(r.indentation); - const increasedIndent = config.normalizeIndentation(r.indentation + r.appendText); - - const typeText = '\n' + increasedIndent + '\n' + normalIndent; - - if (keepPosition) { - return new ReplaceCommandWithoutChangingPosition(range, typeText, true); - } else { - return new ReplaceCommandWithOffsetCursorState(range, typeText, -1, increasedIndent.length - normalIndent.length, true); - } - } else if (r.indentAction === IndentAction.Outdent) { - const actualIndentation = TypeOperations.unshiftIndent(config, r.indentation); - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(actualIndentation + r.appendText), keepPosition); - } - } - - const lineText = model.getLineContent(range.startLineNumber); - const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); - - if (config.autoIndent >= EditorAutoIndentStrategy.Full) { - const ir = getIndentForEnter(config.autoIndent, model, range, { - unshiftIndent: (indent) => { - return TypeOperations.unshiftIndent(config, indent); - }, - shiftIndent: (indent) => { - return TypeOperations.shiftIndent(config, indent); - }, - normalizeIndentation: (indent) => { - return config.normalizeIndentation(indent); - } - }, config.languageConfigurationService); - - if (ir) { - let oldEndViewColumn = config.visibleColumnFromColumn(model, range.getEndPosition()); - const oldEndColumn = range.endColumn; - const newLineContent = model.getLineContent(range.endLineNumber); - const firstNonWhitespace = strings.firstNonWhitespaceIndex(newLineContent); - if (firstNonWhitespace >= 0) { - range = range.setEndPosition(range.endLineNumber, Math.max(range.endColumn, firstNonWhitespace + 1)); - } else { - range = range.setEndPosition(range.endLineNumber, model.getLineMaxColumn(range.endLineNumber)); - } - - if (keepPosition) { - return new ReplaceCommandWithoutChangingPosition(range, '\n' + config.normalizeIndentation(ir.afterEnter), true); - } else { - let offset = 0; - if (oldEndColumn <= firstNonWhitespace + 1) { - if (!config.insertSpaces) { - oldEndViewColumn = Math.ceil(oldEndViewColumn / config.indentSize); - } - offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0); - } - return new ReplaceCommandWithOffsetCursorState(range, '\n' + config.normalizeIndentation(ir.afterEnter), 0, offset, true); - } - } - } - - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); - } - - private static _isAutoIndentType(config: CursorConfiguration, model: ITextModel, selections: Selection[]): boolean { - if (config.autoIndent < EditorAutoIndentStrategy.Full) { - return false; - } - - for (let i = 0, len = selections.length; i < len; i++) { - if (!model.tokenization.isCheapToTokenize(selections[i].getEndPosition().lineNumber)) { - return false; - } - } - - return true; - } - - private static _runAutoIndentType(config: CursorConfiguration, model: ITextModel, range: Range, ch: string): ICommand | null { - const currentIndentation = getIndentationAtPosition(model, range.startLineNumber, range.startColumn); - const actualIndentation = getIndentActionForType(config.autoIndent, model, range, ch, { - shiftIndent: (indentation) => { - return TypeOperations.shiftIndent(config, indentation); - }, - unshiftIndent: (indentation) => { - return TypeOperations.unshiftIndent(config, indentation); - }, - }, config.languageConfigurationService); - - if (actualIndentation === null) { - return null; - } - - if (actualIndentation !== config.normalizeIndentation(currentIndentation)) { - const firstNonWhitespace = model.getLineFirstNonWhitespaceColumn(range.startLineNumber); - if (firstNonWhitespace === 0) { - return TypeOperations._typeCommand( - new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn), - config.normalizeIndentation(actualIndentation) + ch, - false - ); - } else { - return TypeOperations._typeCommand( - new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn), - config.normalizeIndentation(actualIndentation) + - model.getLineContent(range.startLineNumber).substring(firstNonWhitespace - 1, range.startColumn - 1) + ch, - false - ); - } - } - - return null; - } - - private static _isAutoClosingOvertype(config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): boolean { - if (config.autoClosingOvertype === 'never') { - return false; - } - - if (!config.autoClosingPairs.autoClosingPairsCloseSingleChar.has(ch)) { - return false; - } - - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - - if (!selection.isEmpty()) { - return false; - } - - const position = selection.getPosition(); - const lineText = model.getLineContent(position.lineNumber); - const afterCharacter = lineText.charAt(position.column - 1); - - if (afterCharacter !== ch) { - return false; - } - - // Do not over-type quotes after a backslash - const chIsQuote = isQuote(ch); - const beforeCharacter = position.column > 2 ? lineText.charCodeAt(position.column - 2) : CharCode.Null; - if (beforeCharacter === CharCode.Backslash && chIsQuote) { - return false; - } - - // Must over-type a closing character typed by the editor - if (config.autoClosingOvertype === 'auto') { - let found = false; - for (let j = 0, lenJ = autoClosedCharacters.length; j < lenJ; j++) { - const autoClosedCharacter = autoClosedCharacters[j]; - if (position.lineNumber === autoClosedCharacter.startLineNumber && position.column === autoClosedCharacter.startColumn) { - found = true; - break; - } - } - if (!found) { - return false; - } - } - } - - return true; - } - - private static _runAutoClosingOvertype(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - const position = selection.getPosition(); - const typeSelection = new Range(position.lineNumber, position.column, position.lineNumber, position.column + 1); - commands[i] = new ReplaceCommand(typeSelection, ch); - } - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), - shouldPushStackElementAfter: false - }); - } - - private static _isBeforeClosingBrace(config: CursorConfiguration, lineAfter: string) { - // If the start of lineAfter can be interpretted as both a starting or ending brace, default to returning false - const nextChar = lineAfter.charAt(0); - const potentialStartingBraces = config.autoClosingPairs.autoClosingPairsOpenByStart.get(nextChar) || []; - const potentialClosingBraces = config.autoClosingPairs.autoClosingPairsCloseByStart.get(nextChar) || []; - - const isBeforeStartingBrace = potentialStartingBraces.some(x => lineAfter.startsWith(x.open)); - const isBeforeClosingBrace = potentialClosingBraces.some(x => lineAfter.startsWith(x.close)); - - return !isBeforeStartingBrace && isBeforeClosingBrace; - } - - /** - * Determine if typing `ch` at all `positions` in the `model` results in an - * auto closing open sequence being typed. - * - * Auto closing open sequences can consist of multiple characters, which - * can lead to ambiguities. In such a case, the longest auto-closing open - * sequence is returned. - */ - private static _findAutoClosingPairOpen(config: CursorConfiguration, model: ITextModel, positions: Position[], ch: string): StandardAutoClosingPairConditional | null { - const candidates = config.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch); - if (!candidates) { - return null; - } - - // Determine which auto-closing pair it is - let result: StandardAutoClosingPairConditional | null = null; - for (const candidate of candidates) { - if (result === null || candidate.open.length > result.open.length) { - let candidateIsMatch = true; - for (const position of positions) { - const relevantText = model.getValueInRange(new Range(position.lineNumber, position.column - candidate.open.length + 1, position.lineNumber, position.column)); - if (relevantText + ch !== candidate.open) { - candidateIsMatch = false; - break; - } - } - - if (candidateIsMatch) { - result = candidate; - } - } - } - return result; - } - - /** - * Find another auto-closing pair that is contained by the one passed in. - * - * e.g. when having [(,)] and [(*,*)] as auto-closing pairs - * this method will find [(,)] as a containment pair for [(*,*)] - */ - private static _findContainedAutoClosingPair(config: CursorConfiguration, pair: StandardAutoClosingPairConditional): StandardAutoClosingPairConditional | null { - if (pair.open.length <= 1) { - return null; - } - const lastChar = pair.close.charAt(pair.close.length - 1); - // get candidates with the same last character as close - const candidates = config.autoClosingPairs.autoClosingPairsCloseByEnd.get(lastChar) || []; - let result: StandardAutoClosingPairConditional | null = null; - for (const candidate of candidates) { - if (candidate.open !== pair.open && pair.open.includes(candidate.open) && pair.close.endsWith(candidate.close)) { - if (!result || candidate.open.length > result.open.length) { - result = candidate; - } - } - } - return result; - } - - private static _getAutoClosingPairClose(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean): string | null { - - for (const selection of selections) { - if (!selection.isEmpty()) { - return null; - } - } - - // This method is called both when typing (regularly) and when composition ends - // This means that we need to work with a text buffer where sometimes `ch` is not - // there (it is being typed right now) or with a text buffer where `ch` has already been typed - // - // In order to avoid adding checks for `chIsAlreadyTyped` in all places, we will work - // with two conceptual positions, the position before `ch` and the position after `ch` - // - const positions: { lineNumber: number; beforeColumn: number; afterColumn: number }[] = selections.map((s) => { - const position = s.getPosition(); - if (chIsAlreadyTyped) { - return { lineNumber: position.lineNumber, beforeColumn: position.column - ch.length, afterColumn: position.column }; - } else { - return { lineNumber: position.lineNumber, beforeColumn: position.column, afterColumn: position.column }; - } - }); - - - // Find the longest auto-closing open pair in case of multiple ending in `ch` - // e.g. when having [f","] and [","], it picks [f","] if the character before is f - const pair = this._findAutoClosingPairOpen(config, model, positions.map(p => new Position(p.lineNumber, p.beforeColumn)), ch); - if (!pair) { - return null; - } - - let autoCloseConfig: EditorAutoClosingStrategy; - let shouldAutoCloseBefore: (ch: string) => boolean; - - const chIsQuote = isQuote(ch); - if (chIsQuote) { - autoCloseConfig = config.autoClosingQuotes; - shouldAutoCloseBefore = config.shouldAutoCloseBefore.quote; - } else { - const pairIsForComments = config.blockCommentStartToken ? pair.open.includes(config.blockCommentStartToken) : false; - if (pairIsForComments) { - autoCloseConfig = config.autoClosingComments; - shouldAutoCloseBefore = config.shouldAutoCloseBefore.comment; - } else { - autoCloseConfig = config.autoClosingBrackets; - shouldAutoCloseBefore = config.shouldAutoCloseBefore.bracket; - } - } - - if (autoCloseConfig === 'never') { - return null; - } - - // Sometimes, it is possible to have two auto-closing pairs that have a containment relationship - // e.g. when having [(,)] and [(*,*)] - // - when typing (, the resulting state is (|) - // - when typing *, the desired resulting state is (*|*), not (*|*)) - const containedPair = this._findContainedAutoClosingPair(config, pair); - const containedPairClose = containedPair ? containedPair.close : ''; - let isContainedPairPresent = true; - - for (const position of positions) { - const { lineNumber, beforeColumn, afterColumn } = position; - const lineText = model.getLineContent(lineNumber); - const lineBefore = lineText.substring(0, beforeColumn - 1); - const lineAfter = lineText.substring(afterColumn - 1); - - if (!lineAfter.startsWith(containedPairClose)) { - isContainedPairPresent = false; - } - - // Only consider auto closing the pair if an allowed character follows or if another autoclosed pair closing brace follows - if (lineAfter.length > 0) { - const characterAfter = lineAfter.charAt(0); - const isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, lineAfter); - - if (!isBeforeCloseBrace && !shouldAutoCloseBefore(characterAfter)) { - return null; - } - } - - // Do not auto-close ' or " after a word character - if (pair.open.length === 1 && (ch === '\'' || ch === '"') && autoCloseConfig !== 'always') { - const wordSeparators = getMapForWordSeparators(config.wordSeparators, []); - if (lineBefore.length > 0) { - const characterBefore = lineBefore.charCodeAt(lineBefore.length - 1); - if (wordSeparators.get(characterBefore) === WordCharacterClass.Regular) { - return null; - } - } - } - - if (!model.tokenization.isCheapToTokenize(lineNumber)) { - // Do not force tokenization - return null; - } - - model.tokenization.forceTokenization(lineNumber); - const lineTokens = model.tokenization.getLineTokens(lineNumber); - const scopedLineTokens = createScopedLineTokens(lineTokens, beforeColumn - 1); - if (!pair.shouldAutoClose(scopedLineTokens, beforeColumn - scopedLineTokens.firstCharOffset)) { - return null; - } - - // Typing for example a quote could either start a new string, in which case auto-closing is desirable - // or it could end a previously started string, in which case auto-closing is not desirable - // - // In certain cases, it is really not possible to look at the previous token to determine - // what would happen. That's why we do something really unusual, we pretend to type a different - // character and ask the tokenizer what the outcome of doing that is: after typing a neutral - // character, are we in a string (i.e. the quote would most likely end a string) or not? - // - const neutralCharacter = pair.findNeutralCharacter(); - if (neutralCharacter) { - const tokenType = model.tokenization.getTokenTypeIfInsertingCharacter(lineNumber, beforeColumn, neutralCharacter); - if (!pair.isOK(tokenType)) { - return null; - } - } - } - - if (isContainedPairPresent) { - return pair.close.substring(0, pair.close.length - containedPairClose.length); - } else { - return pair.close; - } - } - - private static _runAutoClosingOpenCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean, autoClosingPairClose: string): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - commands[i] = new TypeWithAutoClosingCommand(selection, ch, !chIsAlreadyTyped, autoClosingPairClose); - } - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: false - }); - } - - private static _shouldSurroundChar(config: CursorConfiguration, ch: string): boolean { - if (isQuote(ch)) { - return (config.autoSurround === 'quotes' || config.autoSurround === 'languageDefined'); - } else { - // Character is a bracket - return (config.autoSurround === 'brackets' || config.autoSurround === 'languageDefined'); - } - } - - private static _isSurroundSelectionType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): boolean { - if (!TypeOperations._shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) { - return false; - } - - const isTypingAQuoteCharacter = isQuote(ch); - - for (const selection of selections) { - - if (selection.isEmpty()) { - return false; - } - - let selectionContainsOnlyWhitespace = true; - - for (let lineNumber = selection.startLineNumber; lineNumber <= selection.endLineNumber; lineNumber++) { - const lineText = model.getLineContent(lineNumber); - const startIndex = (lineNumber === selection.startLineNumber ? selection.startColumn - 1 : 0); - const endIndex = (lineNumber === selection.endLineNumber ? selection.endColumn - 1 : lineText.length); - const selectedText = lineText.substring(startIndex, endIndex); - if (/[^ \t]/.test(selectedText)) { - // this selected text contains something other than whitespace - selectionContainsOnlyWhitespace = false; - break; - } - } - - if (selectionContainsOnlyWhitespace) { - return false; - } - - if (isTypingAQuoteCharacter && selection.startLineNumber === selection.endLineNumber && selection.startColumn + 1 === selection.endColumn) { - const selectionText = model.getValueInRange(selection); - if (isQuote(selectionText)) { - // Typing a quote character on top of another quote character - // => disable surround selection type - return false; - } - } - } - - return true; - } - - private static _runSurroundSelectionType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - const closeCharacter = config.surroundingPairs[ch]; - commands[i] = new SurroundSelectionCommand(selection, ch, closeCharacter); - } - return new EditOperationResult(EditOperationType.Other, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: true - }); - } - - private static _isTypeInterceptorElectricChar(config: CursorConfiguration, model: ITextModel, selections: Selection[]) { - if (selections.length === 1 && model.tokenization.isCheapToTokenize(selections[0].getEndPosition().lineNumber)) { - return true; - } - return false; - } - - private static _typeInterceptorElectricChar(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selection: Selection, ch: string): EditOperationResult | null { - if (!config.electricChars.hasOwnProperty(ch) || !selection.isEmpty()) { - return null; - } - - const position = selection.getPosition(); - model.tokenization.forceTokenization(position.lineNumber); - const lineTokens = model.tokenization.getLineTokens(position.lineNumber); - - let electricAction: IElectricAction | null; - try { - electricAction = config.onElectricCharacter(ch, lineTokens, position.column); - } catch (e) { - onUnexpectedError(e); - return null; - } - - if (!electricAction) { - return null; - } - - if (electricAction.matchOpenBracket) { - const endColumn = (lineTokens.getLineContent() + ch).lastIndexOf(electricAction.matchOpenBracket) + 1; - const match = model.bracketPairs.findMatchingBracketUp(electricAction.matchOpenBracket, { - lineNumber: position.lineNumber, - column: endColumn - }, 500 /* give at most 500ms to compute */); - - if (match) { - if (match.startLineNumber === position.lineNumber) { - // matched something on the same line => no change in indentation - return null; - } - const matchLine = model.getLineContent(match.startLineNumber); - const matchLineIndentation = strings.getLeadingWhitespace(matchLine); - const newIndentation = config.normalizeIndentation(matchLineIndentation); - - const lineText = model.getLineContent(position.lineNumber); - const lineFirstNonBlankColumn = model.getLineFirstNonWhitespaceColumn(position.lineNumber) || position.column; - - const prefix = lineText.substring(lineFirstNonBlankColumn - 1, position.column - 1); - const typeText = newIndentation + prefix + ch; - - const typeSelection = new Range(position.lineNumber, 1, position.lineNumber, position.column); - - const command = new ReplaceCommand(typeSelection, typeText); - return new EditOperationResult(getTypingOperation(typeText, prevEditOperationType), [command], { - shouldPushStackElementBefore: false, - shouldPushStackElementAfter: true - }); - } - } - - return null; + return CompositionOperation.getEdits(prevEditOperationType, config, model, selections, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta); } /** @@ -871,7 +106,7 @@ export class TypeOperations { if (hasDeletion) { // Check if this could have been a surround selection - if (!TypeOperations._shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) { + if (!shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) { return null; } @@ -914,18 +149,14 @@ export class TypeOperations { }); } - if (this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { - // Unfortunately, the close character is at this point "doubled", so we need to delete it... - const commands = selections.map(s => new ReplaceCommand(new Range(s.positionLineNumber, s.positionColumn, s.positionLineNumber, s.positionColumn + 1), '', false)); - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: false - }); + const autoClosingOvertypeEdits = AutoClosingOvertypeWithInterceptorsOperation.getEdits(config, model, selections, autoClosedCharacters, ch); + if (autoClosingOvertypeEdits !== undefined) { + return autoClosingOvertypeEdits; } - const autoClosingPairClose = this._getAutoClosingPairClose(config, model, selections, ch, true); - if (autoClosingPairClose !== null) { - return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, true, autoClosingPairClose); + const autoClosingOpenCharEdits = AutoClosingOpenCharTypeOperation.getEdits(config, model, selections, ch, true, false); + if (autoClosingOpenCharEdits !== undefined) { + return autoClosingOpenCharEdits; } return null; @@ -933,149 +164,41 @@ export class TypeOperations { public static typeWithInterceptors(isDoingComposition: boolean, prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult { - if (!isDoingComposition && ch === '\n') { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = TypeOperations._enter(config, model, false, selections[i]); - } - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: false, - }); + const enterEdits = EnterOperation.getEdits(config, model, selections, ch, isDoingComposition); + if (enterEdits !== undefined) { + return enterEdits; } - if (!isDoingComposition && this._isAutoIndentType(config, model, selections)) { - const commands: Array = []; - let autoIndentFails = false; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = this._runAutoIndentType(config, model, selections[i], ch); - if (!commands[i]) { - autoIndentFails = true; - break; - } - } - if (!autoIndentFails) { - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: false, - }); - } + const autoIndentEdits = AutoIndentOperation.getEdits(config, model, selections, ch, isDoingComposition); + if (autoIndentEdits !== undefined) { + return autoIndentEdits; } - if (this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { - return this._runAutoClosingOvertype(prevEditOperationType, config, model, selections, ch); + const autoClosingOverTypeEdits = AutoClosingOvertypeOperation.getEdits(prevEditOperationType, config, model, selections, autoClosedCharacters, ch); + if (autoClosingOverTypeEdits !== undefined) { + return autoClosingOverTypeEdits; } - if (!isDoingComposition) { - const autoClosingPairClose = this._getAutoClosingPairClose(config, model, selections, ch, false); - if (autoClosingPairClose) { - return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, false, autoClosingPairClose); - } + const autoClosingOpenCharEdits = AutoClosingOpenCharTypeOperation.getEdits(config, model, selections, ch, false, isDoingComposition); + if (autoClosingOpenCharEdits !== undefined) { + return autoClosingOpenCharEdits; } - if (!isDoingComposition && this._isSurroundSelectionType(config, model, selections, ch)) { - return this._runSurroundSelectionType(prevEditOperationType, config, model, selections, ch); + const surroundSelectionEdits = SurroundSelectionOperation.getEdits(config, model, selections, ch, isDoingComposition); + if (surroundSelectionEdits !== undefined) { + return surroundSelectionEdits; } - // Electric characters make sense only when dealing with a single cursor, - // as multiple cursors typing brackets for example would interfer with bracket matching - if (!isDoingComposition && this._isTypeInterceptorElectricChar(config, model, selections)) { - const r = this._typeInterceptorElectricChar(prevEditOperationType, config, model, selections[0], ch); - if (r) { - return r; - } + const interceptorElectricCharOperation = InterceptorElectricCharOperation.getEdits(prevEditOperationType, config, model, selections, ch, isDoingComposition); + if (interceptorElectricCharOperation !== undefined) { + return interceptorElectricCharOperation; } - // 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 opType = getTypingOperation(ch, prevEditOperationType); - return new EditOperationResult(opType, commands, { - shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), - shouldPushStackElementAfter: false - }); + return SimpleCharacterTypeOperation.getEdits(prevEditOperationType, selections, ch); } public static typeWithoutInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], str: string): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = new ReplaceCommand(selections[i], str); - } - const opType = getTypingOperation(str, prevEditOperationType); - return new EditOperationResult(opType, commands, { - shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), - shouldPushStackElementAfter: false - }); - } - - public static lineInsertBefore(config: CursorConfiguration, model: ITextModel | null, selections: Selection[] | null): ICommand[] { - if (model === null || selections === null) { - return []; - } - - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - let lineNumber = selections[i].positionLineNumber; - - if (lineNumber === 1) { - commands[i] = new ReplaceCommandWithoutChangingPosition(new Range(1, 1, 1, 1), '\n'); - } else { - lineNumber--; - const column = model.getLineMaxColumn(lineNumber); - - commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column)); - } - } - return commands; - } - - public static lineInsertAfter(config: CursorConfiguration, model: ITextModel | null, selections: Selection[] | null): ICommand[] { - if (model === null || selections === null) { - return []; - } - - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const lineNumber = selections[i].positionLineNumber; - const column = model.getLineMaxColumn(lineNumber); - commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column)); - } - return commands; - } - - public static lineBreakInsert(config: CursorConfiguration, model: ITextModel, selections: Selection[]): ICommand[] { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = this._enter(config, model, true, selections[i]); - } - return commands; - } -} - -export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorState { - - private readonly _openCharacter: string; - private readonly _closeCharacter: string; - public closeCharacterRange: Range | null; - public enclosingRange: Range | null; - - constructor(selection: Selection, openCharacter: string, insertOpenCharacter: boolean, closeCharacter: string) { - super(selection, (insertOpenCharacter ? openCharacter : '') + closeCharacter, 0, -closeCharacter.length); - this._openCharacter = openCharacter; - this._closeCharacter = closeCharacter; - this.closeCharacterRange = null; - this.enclosingRange = null; - } - - public override computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { - const inverseEditOperations = helper.getInverseEditOperations(); - const range = inverseEditOperations[0].range; - this.closeCharacterRange = new Range(range.startLineNumber, range.endColumn - this._closeCharacter.length, range.endLineNumber, range.endColumn); - this.enclosingRange = new Range(range.startLineNumber, range.endColumn - this._openCharacter.length - this._closeCharacter.length, range.endLineNumber, range.endColumn); - return super.computeCursorState(model, helper); + return TypeWithoutInterceptorsOperation.getEdits(prevEditOperationType, selections, str); } } @@ -1089,40 +212,3 @@ export class CompositionOutcome { public readonly insertedSelectionEnd: number, ) { } } - -function getTypingOperation(typedText: string, previousTypingOperation: EditOperationType): EditOperationType { - if (typedText === ' ') { - return previousTypingOperation === EditOperationType.TypingFirstSpace - || previousTypingOperation === EditOperationType.TypingConsecutiveSpace - ? EditOperationType.TypingConsecutiveSpace - : EditOperationType.TypingFirstSpace; - } - - return EditOperationType.TypingOther; -} - -function shouldPushStackElementBetween(previousTypingOperation: EditOperationType, typingOperation: EditOperationType): boolean { - if (isTypingOperation(previousTypingOperation) && !isTypingOperation(typingOperation)) { - // Always set an undo stop before non-type operations - return true; - } - if (previousTypingOperation === EditOperationType.TypingFirstSpace) { - // `abc |d`: No undo stop - // `abc |d`: Undo stop - return false; - } - // Insert undo stop between different operation types - return normalizeOperationType(previousTypingOperation) !== normalizeOperationType(typingOperation); -} - -function normalizeOperationType(type: EditOperationType): EditOperationType | 'space' { - return (type === EditOperationType.TypingConsecutiveSpace || type === EditOperationType.TypingFirstSpace) - ? 'space' - : type; -} - -function isTypingOperation(type: EditOperationType): boolean { - return type === EditOperationType.TypingOther - || type === EditOperationType.TypingFirstSpace - || type === EditOperationType.TypingConsecutiveSpace; -} diff --git a/src/vs/editor/common/cursor/cursorWordOperations.ts b/src/vs/editor/common/cursor/cursorWordOperations.ts index b16172cc89a..a43538215b2 100644 --- a/src/vs/editor/common/cursor/cursorWordOperations.ts +++ b/src/vs/editor/common/cursor/cursorWordOperations.ts @@ -208,7 +208,7 @@ export class WordOperations { return 0; } - public static moveWordLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position { + public static moveWordLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { let lineNumber = position.lineNumber; let column = position.column; @@ -227,7 +227,8 @@ export class WordOperations { if (wordNavigationType === WordNavigationType.WordStartFast) { if ( - prevWordOnLine + !hasMulticursor // avoid having multiple cursors stop at different locations when doing word start + && prevWordOnLine && prevWordOnLine.wordType === WordType.Separator && prevWordOnLine.end - prevWordOnLine.start === 1 && prevWordOnLine.nextCharClass === WordCharacterClass.Regular @@ -830,10 +831,10 @@ export class WordPartOperations extends WordOperations { return candidates[0]; } - public static moveWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position): Position { + public static moveWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, hasMulticursor: boolean): Position { const candidates = enforceDefined([ - WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordStart), - WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordEnd), + WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordStart, hasMulticursor), + WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordEnd, hasMulticursor), WordOperations._moveWordPartLeft(model, position) ]); candidates.sort(Position.compare); diff --git a/src/vs/editor/common/languageFeatureRegistry.ts b/src/vs/editor/common/languageFeatureRegistry.ts index 53c14ac57b9..47679f308f4 100644 --- a/src/vs/editor/common/languageFeatureRegistry.ts +++ b/src/vs/editor/common/languageFeatureRegistry.ts @@ -109,6 +109,10 @@ export class LanguageFeatureRegistry { return result; } + allNoModel(): T[] { + return this._entries.map(entry => entry.provider); + } + ordered(model: ITextModel): T[] { const result: T[] = []; this._orderedForEach(model, entry => result.push(entry.provider)); diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index b4af6c08bb7..fb88b4e7d94 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -687,6 +687,11 @@ export interface InlineCompletionContext { */ readonly triggerKind: InlineCompletionTriggerKind; readonly selectedSuggestionInfo: SelectedSuggestionInfo | undefined; + /** + * @experimental + * @internal + */ + readonly userPrompt?: string | undefined; } export class SelectedSuggestionInfo { @@ -765,6 +770,12 @@ export type InlineCompletionProviderGroupId = string; export interface InlineCompletionsProvider { provideInlineCompletions(model: model.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + /** + * @experimental + * @internal + */ + provideInlineEdits?(model: model.ITextModel, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + /** * Will be called when an item is shown. * @param updatedInsertText Is useful to understand bracket completion. @@ -1880,6 +1891,11 @@ export interface CommentInput { uri: URI; } +export interface CommentThreadRevealOptions { + preserveFocus: boolean; + focusReply: boolean; +} + /** * @internal */ @@ -2242,6 +2258,14 @@ export interface DocumentDropEdit { additionalEdit?: WorkspaceEdit; } +/** + * @internal + */ +export interface DocumentDropEditsSession { + edits: readonly DocumentDropEdit[]; + dispose(): void; +} + /** * @internal */ @@ -2249,7 +2273,7 @@ export interface DocumentDropEditProvider { readonly id?: string; readonly dropMimeTypes?: readonly string[]; - provideDocumentDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult; + provideDocumentDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult; resolveDocumentDropEdit?(edit: DocumentDropEdit, token: CancellationToken): Promise; } @@ -2271,7 +2295,7 @@ export interface MappedEditsProvider { * * @param document The document to provide mapped edits for. * @param codeBlocks Code blocks that come from an LLM's reply. - * "Insert at cursor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. + * "Apply in Editor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. * @param context The context for providing mapped edits. * @param token A cancellation token. * @returns A provider result of text edits. diff --git a/src/vs/editor/common/languages/autoIndent.ts b/src/vs/editor/common/languages/autoIndent.ts index bf0b8141435..1407ab7c41b 100644 --- a/src/vs/editor/common/languages/autoIndent.ts +++ b/src/vs/editor/common/languages/autoIndent.ts @@ -12,6 +12,7 @@ import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions' import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { IndentationContextProcessor, isLanguageDifferentFromLineStart, ProcessedIndentRulesSupport } from 'vs/editor/common/languages/supports/indentationLineProcessor'; +import { CursorConfiguration } from 'vs/editor/common/cursorCommon'; export interface IVirtualModel { tokenization: { @@ -390,13 +391,14 @@ export function getIndentForEnter( * this line doesn't match decreaseIndentPattern, we should not adjust the indentation. */ export function getIndentActionForType( - autoIndent: EditorAutoIndentStrategy, + cursorConfig: CursorConfiguration, model: ITextModel, range: Range, ch: string, indentConverter: IIndentConverter, languageConfigurationService: ILanguageConfigurationService ): string | null { + const autoIndent = cursorConfig.autoIndent; if (autoIndent < EditorAutoIndentStrategy.Full) { return null; } @@ -439,6 +441,29 @@ export function getIndentActionForType( return indentation; } + const previousLineNumber = range.startLineNumber - 1; + if (previousLineNumber > 0) { + const previousLine = model.getLineContent(previousLineNumber); + if (indentRulesSupport.shouldIndentNextLine(previousLine) && indentRulesSupport.shouldIncrease(textAroundRangeWithCharacter)) { + const inheritedIndentationData = getInheritIndentForLine(autoIndent, model, range.startLineNumber, false, languageConfigurationService); + const inheritedIndentation = inheritedIndentationData?.indentation; + if (inheritedIndentation !== undefined) { + const currentLine = model.getLineContent(range.startLineNumber); + const actualCurrentIndentation = strings.getLeadingWhitespace(currentLine); + const inferredCurrentIndentation = indentConverter.shiftIndent(inheritedIndentation); + // If the inferred current indentation is not equal to the actual current indentation, then the indentation has been intentionally changed, in that case keep it + const inferredIndentationEqualsActual = inferredCurrentIndentation === actualCurrentIndentation; + const textAroundRangeContainsOnlyWhitespace = /^\s*$/.test(textAroundRange); + const autoClosingPairs = cursorConfig.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch); + const autoClosingPairExists = autoClosingPairs && autoClosingPairs.length > 0; + const isChFirstNonWhitespaceCharacterAndInAutoClosingPair = autoClosingPairExists && textAroundRangeContainsOnlyWhitespace; + if (inferredIndentationEqualsActual && isChFirstNonWhitespaceCharacterAndInAutoClosingPair) { + return inheritedIndentation; + } + } + } + } + return null; } diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index e21aa7d600c..134234bbfe5 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -1431,6 +1431,11 @@ export interface IReadonlyTextBuffer { getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[]; + + /** + * Get nearest chunk of text after `offset` in the text buffer. + */ + getNearestChunk(offset: number): string; } /** diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts index 470a16f009c..3d3fe2e3649 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts @@ -8,7 +8,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService, LanguageConfigurationServiceChangeEvent } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ignoreBracketsInToken } from 'vs/editor/common/languages/supports'; import { LanguageBracketsConfiguration } from 'vs/editor/common/languages/supports/languageBracketsConfiguration'; import { BracketsUtils, RichEditBracket, RichEditBrackets } from 'vs/editor/common/languages/supports/richEditBrackets'; @@ -36,19 +36,17 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai private readonly languageConfigurationService: ILanguageConfigurationService ) { super(); - - this._register( - this.languageConfigurationService.onDidChange(e => { - if (!e.languageId || this.bracketPairsTree.value?.object.didLanguageChange(e.languageId)) { - this.bracketPairsTree.clear(); - this.updateBracketPairsTree(); - } - }) - ); } //#region TextModel events + public handleLanguageConfigurationServiceChange(e: LanguageConfigurationServiceChangeEvent): void { + if (!e.languageId || this.bracketPairsTree.value?.object.didLanguageChange(e.languageId)) { + this.bracketPairsTree.clear(); + this.updateBracketPairsTree(); + } + } + public handleDidChangeOptions(e: IModelOptionsChangedEvent): void { this.bracketPairsTree.clear(); this.updateBracketPairsTree(); diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index b75d0d75a70..24f90651f95 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -666,6 +666,27 @@ export class PieceTreeBase { return this._getCharCode(nodePos); } + public getNearestChunk(offset: number): string { + const nodePos = this.nodeAt(offset); + if (nodePos.remainder === nodePos.node.piece.length) { + // the offset is at the head of next node. + const matchingNode = nodePos.node.next(); + if (!matchingNode || matchingNode === SENTINEL) { + return ''; + } + + const buffer = this._buffers[matchingNode.piece.bufferIndex]; + const startOffset = this.offsetInBuffer(matchingNode.piece.bufferIndex, matchingNode.piece.start); + return buffer.buffer.substring(startOffset, startOffset + matchingNode.piece.length); + } else { + const buffer = this._buffers[nodePos.node.piece.bufferIndex]; + const startOffset = this.offsetInBuffer(nodePos.node.piece.bufferIndex, nodePos.node.piece.start); + const targetOffset = startOffset + nodePos.remainder; + const targetEnd = startOffset + nodePos.node.piece.length; + return buffer.buffer.substring(targetOffset, targetEnd); + } + } + public findMatchesInNode(node: TreeNode, searcher: Searcher, startLineNumber: number, startColumn: number, startCursor: BufferCursor, endCursor: BufferCursor, searchData: SearchData, captureMatches: boolean, limitResultCount: number, resultLen: number, result: FindMatch[]) { const buffer = this._buffers[node.piece.bufferIndex]; const startOffsetInBuffer = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start); diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts index 12d7e0b0981..a369298c0c9 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -167,6 +167,10 @@ export class PieceTreeTextBuffer extends Disposable implements ITextBuffer { return this.getValueLengthInRange(range, eol); } + public getNearestChunk(offset: number): string { + return this._pieceTree.getNearestChunk(offset); + } + public getLength(): number { return this._pieceTree.getLength(); } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 626217e8bff..97b5a483fc3 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -381,6 +381,11 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati })); this._languageService.requestRichLanguageFeatures(languageId); + + this._register(this._languageConfigurationService.onDidChange(e => { + this._bracketPairs.handleLanguageConfigurationServiceChange(e); + this._tokenizationTextModelPart.handleLanguageConfigurationServiceChange(e); + })); } public override dispose(): void { @@ -413,7 +418,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private _assertNotDisposed(): void { if (this._isDisposed) { - throw new Error('Model is disposed!'); + throw new BugIndicatingError('Model is disposed!'); } } diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts index 804f63c6a28..40c6c921afc 100644 --- a/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -17,7 +17,7 @@ import { IWordAtPosition, getWordAtText } from 'vs/editor/common/core/wordHelper import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { IBackgroundTokenizationStore, IBackgroundTokenizer, ILanguageIdCodec, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService, LanguageConfigurationServiceChangeEvent, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IAttachedView } from 'vs/editor/common/model'; import { BracketPairsTextModelPart } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl'; import { AttachedViews, IAttachedViewState, TextModel } from 'vs/editor/common/model/textModel'; @@ -56,12 +56,6 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz ) { super(); - this._register(this._languageConfigurationService.onDidChange(e => { - if (e.affects(this._languageId)) { - this._onDidChangeLanguageConfiguration.fire({}); - } - })); - this._register(this.grammarTokens.onDidChangeTokens(e => { this._emitModelTokensChangedEvent(e); })); @@ -77,6 +71,12 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz || this._onDidChangeTokens.hasListeners()); } + public handleLanguageConfigurationServiceChange(e: LanguageConfigurationServiceChangeEvent): void { + if (e.affects(this._languageId)) { + this._onDidChangeLanguageConfiguration.fire({}); + } + } + public handleDidChangeContent(e: IModelContentChangedEvent): void { if (e.isFlush) { this._semanticTokens.flush(); diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 3608f3042a8..52a1e2633e6 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -123,5 +123,5 @@ function detectLanguageId(modelService: IModelService, languageService: ILanguag } function cssEscape(str: string): string { - return str.replace(/[\11\12\14\15\40]/g, '/'); // HTML class names can not contain certain whitespace characters, use / instead, which doesn't exist in file names. + return str.replace(/[\x11\x12\x14\x15\x40]/g, '/'); // HTML class names can not contain certain whitespace characters, use / instead, which doesn't exist in file names. } diff --git a/src/vs/editor/common/services/semanticTokensProviderStyling.ts b/src/vs/editor/common/services/semanticTokensProviderStyling.ts index f248e0e23c2..1bb2e0d6ed1 100644 --- a/src/vs/editor/common/services/semanticTokensProviderStyling.ts +++ b/src/vs/editor/common/services/semanticTokensProviderStyling.ts @@ -14,6 +14,8 @@ const enum SemanticTokensProviderStylingConstants { NO_STYLING = 0b01111111111111111111111111111111 } +const ENABLE_TRACE = false; + export class SemanticTokensProviderStyling { private readonly _hashTable: HashTable; @@ -36,7 +38,7 @@ export class SemanticTokensProviderStyling { let metadata: number; if (entry) { metadata = entry.metadata; - if (this._logService.getLevel() === LogLevel.Trace) { + if (ENABLE_TRACE && this._logService.getLevel() === LogLevel.Trace) { this._logService.trace(`SemanticTokensProviderStyling [CACHED] ${tokenTypeIndex} / ${tokenModifierSet}: foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`); } } else { @@ -50,7 +52,7 @@ export class SemanticTokensProviderStyling { } modifierSet = modifierSet >> 1; } - if (modifierSet > 0 && this._logService.getLevel() === LogLevel.Trace) { + if (ENABLE_TRACE && modifierSet > 0 && this._logService.getLevel() === LogLevel.Trace) { this._logService.trace(`SemanticTokensProviderStyling: unknown token modifier index: ${tokenModifierSet.toString(2)} for legend: ${JSON.stringify(this._legend.tokenModifiers)}`); tokenModifiers.push('not-in-legend'); } @@ -86,7 +88,7 @@ export class SemanticTokensProviderStyling { } } } else { - if (this._logService.getLevel() === LogLevel.Trace) { + if (ENABLE_TRACE && this._logService.getLevel() === LogLevel.Trace) { this._logService.trace(`SemanticTokensProviderStyling: unknown token type index: ${tokenTypeIndex} for legend: ${JSON.stringify(this._legend.tokenTypes)}`); } metadata = SemanticTokensProviderStylingConstants.NO_STYLING; @@ -94,7 +96,7 @@ export class SemanticTokensProviderStyling { } this._hashTable.add(tokenTypeIndex, tokenModifierSet, encodedLanguageId, metadata); - if (this._logService.getLevel() === LogLevel.Trace) { + if (ENABLE_TRACE && this._logService.getLevel() === LogLevel.Trace) { this._logService.trace(`SemanticTokensProviderStyling ${tokenTypeIndex} (${tokenType}) / ${tokenModifierSet} (${tokenModifiers.join(' ')}): foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`); } } diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index e5364642311..abc15e5a670 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -261,70 +261,71 @@ export enum EditorOption { pasteAs = 85, parameterHints = 86, peekWidgetDefaultFocus = 87, - definitionLinkOpensInPeek = 88, - quickSuggestions = 89, - quickSuggestionsDelay = 90, - readOnly = 91, - readOnlyMessage = 92, - renameOnType = 93, - renderControlCharacters = 94, - renderFinalNewline = 95, - renderLineHighlight = 96, - renderLineHighlightOnlyWhenFocus = 97, - renderValidationDecorations = 98, - renderWhitespace = 99, - revealHorizontalRightPadding = 100, - roundedSelection = 101, - rulers = 102, - scrollbar = 103, - scrollBeyondLastColumn = 104, - scrollBeyondLastLine = 105, - scrollPredominantAxis = 106, - selectionClipboard = 107, - selectionHighlight = 108, - selectOnLineNumbers = 109, - showFoldingControls = 110, - showUnused = 111, - snippetSuggestions = 112, - smartSelect = 113, - smoothScrolling = 114, - stickyScroll = 115, - stickyTabStops = 116, - stopRenderingLineAfter = 117, - suggest = 118, - suggestFontSize = 119, - suggestLineHeight = 120, - suggestOnTriggerCharacters = 121, - suggestSelection = 122, - tabCompletion = 123, - tabIndex = 124, - unicodeHighlighting = 125, - unusualLineTerminators = 126, - useShadowDOM = 127, - useTabStops = 128, - wordBreak = 129, - wordSegmenterLocales = 130, - wordSeparators = 131, - wordWrap = 132, - wordWrapBreakAfterCharacters = 133, - wordWrapBreakBeforeCharacters = 134, - wordWrapColumn = 135, - wordWrapOverride1 = 136, - wordWrapOverride2 = 137, - wrappingIndent = 138, - wrappingStrategy = 139, - showDeprecated = 140, - inlayHints = 141, - editorClassName = 142, - pixelRatio = 143, - tabFocusMode = 144, - layoutInfo = 145, - wrappingInfo = 146, - defaultColorDecorators = 147, - colorDecoratorsActivatedOn = 148, - inlineCompletionsAccessibilityVerbose = 149, - quickSuggestionsMinimumLength = 150, - tabSuggest = 151 + placeholder = 88, + definitionLinkOpensInPeek = 89, + quickSuggestions = 90, + quickSuggestionsDelay = 91, + readOnly = 92, + readOnlyMessage = 93, + renameOnType = 94, + renderControlCharacters = 95, + renderFinalNewline = 96, + renderLineHighlight = 97, + renderLineHighlightOnlyWhenFocus = 98, + renderValidationDecorations = 99, + renderWhitespace = 100, + revealHorizontalRightPadding = 101, + roundedSelection = 102, + rulers = 103, + scrollbar = 104, + scrollBeyondLastColumn = 105, + scrollBeyondLastLine = 106, + scrollPredominantAxis = 107, + selectionClipboard = 108, + selectionHighlight = 109, + selectOnLineNumbers = 110, + showFoldingControls = 111, + showUnused = 112, + snippetSuggestions = 113, + smartSelect = 114, + smoothScrolling = 115, + stickyScroll = 116, + stickyTabStops = 117, + stopRenderingLineAfter = 118, + suggest = 119, + suggestFontSize = 120, + suggestLineHeight = 121, + suggestOnTriggerCharacters = 122, + suggestSelection = 123, + tabCompletion = 124, + tabIndex = 125, + unicodeHighlighting = 126, + unusualLineTerminators = 127, + useShadowDOM = 128, + useTabStops = 129, + wordBreak = 130, + wordSegmenterLocales = 131, + wordSeparators = 132, + wordWrap = 133, + wordWrapBreakAfterCharacters = 134, + wordWrapBreakBeforeCharacters = 135, + wordWrapColumn = 136, + wordWrapOverride1 = 137, + wordWrapOverride2 = 138, + wrappingIndent = 139, + wrappingStrategy = 140, + showDeprecated = 141, + inlayHints = 142, + editorClassName = 143, + pixelRatio = 144, + tabFocusMode = 145, + layoutInfo = 146, + wrappingInfo = 147, + defaultColorDecorators = 148, + colorDecoratorsActivatedOn = 149, + inlineCompletionsAccessibilityVerbose = 150, + quickSuggestionsMinimumLength = 151, + tabSuggest = 152 } /** @@ -977,4 +978,4 @@ export enum WrappingIndent { * DeepIndent => wrapped lines get +2 indentation toward the parent. */ DeepIndent = 3 -} +} \ No newline at end of file diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index c5af99cca77..50366d54958 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -71,6 +71,7 @@ export class ViewModel extends Disposable implements IViewModel { private readonly languageConfigurationService: ILanguageConfigurationService, private readonly _themeService: IThemeService, private readonly _attachedView: IAttachedView, + private readonly _transactionalTarget: IBatchableTarget, ) { super(); @@ -1102,12 +1103,14 @@ export class ViewModel extends Disposable implements IViewModel { //#endregion private _withViewEventsCollector(callback: (eventsCollector: ViewModelEventsCollector) => T): T { - try { - const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); - return callback(eventsCollector); - } finally { - this._eventDispatcher.endEmitViewEvents(); - } + return this._transactionalTarget.batchChanges(() => { + try { + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); + return callback(eventsCollector); + } finally { + this._eventDispatcher.endEmitViewEvents(); + } + }); } public batchEvents(callback: () => void): void { @@ -1127,6 +1130,13 @@ export class ViewModel extends Disposable implements IViewModel { } } +export interface IBatchableTarget { + /** + * Allows the target to apply the changes introduced by the callback in a batch. + */ + batchChanges(cb: () => T): T; +} + class ViewportStart implements IDisposable { public static create(model: ITextModel): ViewportStart { diff --git a/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts index ffd9e3240dd..b3530eaa888 100644 --- a/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts @@ -23,7 +23,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; -const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', { dark: '#A0A0A0', light: '#A0A0A0', hcDark: '#A0A0A0', hcLight: '#A0A0A0' }, nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.')); +const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', '#A0A0A0', nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.')); class JumpToBracketAction extends EditorAction { constructor() { diff --git a/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts b/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts index 289fe8aa96a..7a5f7e106e7 100644 --- a/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts +++ b/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index 65dc839da2c..612fd4d2923 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts @@ -39,8 +39,6 @@ import { registerThemingParticipant } from 'vs/platform/theme/common/themeServic import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; import { CodeActionModel, CodeActionsState } from 'vs/editor/contrib/codeAction/browser/codeActionModel'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; - interface IActionShowOptions { readonly includeDisabledActions?: boolean; @@ -79,8 +77,7 @@ export class CodeActionController extends Disposable implements IEditorContribut @ICommandService private readonly _commandService: ICommandService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IActionWidgetService private readonly _actionWidgetService: IActionWidgetService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ITelemetryService private readonly _telemetryService: ITelemetryService + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); @@ -107,29 +104,6 @@ export class CodeActionController extends Disposable implements IEditorContribut } private async showCodeActionsFromLightbulb(actions: CodeActionSet, at: IAnchor | IPosition): Promise { - - // Telemetry for showing code actions from lightbulb. Shows us how often it was clicked. - type ShowCodeActionListEvent = { - codeActionListLength: number; - codeActions: string[]; - codeActionProviders: string[]; - }; - - type ShowListEventClassification = { - codeActionListLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The length of the code action list from the lightbulb widget.' }; - codeActions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The title of code actions in this menu.' }; - codeActionProviders: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider of code actions in this menu.' }; - owner: 'justschen'; - comment: 'Event used to gain insights into what code actions are being shown'; - }; - - this._telemetryService.publicLog2('codeAction.showCodeActionsFromLightbulb', { - codeActionListLength: actions.validActions.length, - codeActions: actions.validActions.map(action => action.action.title), - codeActionProviders: actions.validActions.map(action => action.provider?.displayName ?? ''), - }); - - if (actions.allAIFixes && actions.validActions.length === 1) { const actionItem = actions.validActions[0]; const command = actionItem.action.command; @@ -312,28 +286,6 @@ export class CodeActionController extends Disposable implements IEditorContribut onHide: (didCancel?) => { this._editor?.focus(); currentDecorations.clear(); - // Telemetry for showing code actions here. only log on `showLightbulb`. Logs when code action list is quit out. - if (options.fromLightbulb && didCancel !== undefined) { - type ShowCodeActionListEvent = { - codeActionListLength: number; - didCancel: boolean; - codeActions: string[]; - }; - - type ShowListEventClassification = { - codeActionListLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The length of the code action list when quit out. Can be from any code action menu.' }; - didCancel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the code action was cancelled or selected.' }; - codeActions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'What code actions were available when cancelled.' }; - owner: 'justschen'; - comment: 'Event used to gain insights into how many valid code actions are being shown'; - }; - - this._telemetryService.publicLog2('codeAction.showCodeActionList.onHide', { - codeActionListLength: actions.validActions.length, - didCancel: didCancel, - codeActions: actions.validActions.map(action => action.action.title), - }); - } }, onHover: async (action: CodeActionItem, token: CancellationToken) => { if (token.isCancellationRequested) { diff --git a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css index ea05c8574b7..cbadb2348ef 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css @@ -41,6 +41,5 @@ width: 100%; height: 100%; opacity: 0.3; - background-color: var(--vscode-editor-background); z-index: 1; } diff --git a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts index 94518efd415..45fc9aa9cb7 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts @@ -17,7 +17,6 @@ import { computeIndentLevel } from 'vs/editor/common/model/utils'; import { autoFixCommandId, quickFixCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { CodeActionSet, CodeActionTrigger } from 'vs/editor/contrib/codeAction/common/types'; import * as nls from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; namespace LightBulbState { @@ -62,8 +61,7 @@ export class LightBulbWidget extends Disposable implements IContentWidget { constructor( private readonly _editor: ICodeEditor, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @ICommandService commandService: ICommandService + @IKeybindingService private readonly _keybindingService: IKeybindingService ) { super(); @@ -173,8 +171,27 @@ export class LightBulbWidget extends Disposable implements IContentWidget { let effectiveLineNumber = lineNumber; let effectiveColumnNumber = 1; if (!lineHasSpace) { + + // Checks if line is empty or starts with any amount of whitespace + const isLineEmptyOrIndented = (lineNumber: number): boolean => { + const lineContent = model.getLineContent(lineNumber); + return /^\s*$|^\s+/.test(lineContent) || lineContent.length <= effectiveColumnNumber; + }; + if (lineNumber > 1 && !isFolded(lineNumber - 1)) { - effectiveLineNumber -= 1; + const lineCount = model.getLineCount(); + const endLine = lineNumber === lineCount; + const prevLineEmptyOrIndented = lineNumber > 1 && isLineEmptyOrIndented(lineNumber - 1); + const nextLineEmptyOrIndented = !endLine && isLineEmptyOrIndented(lineNumber + 1); + const currLineEmptyOrIndented = isLineEmptyOrIndented(lineNumber); + const notEmpty = !nextLineEmptyOrIndented && !prevLineEmptyOrIndented; + + // check above and below. if both are blocked, display lightbulb below. + if (prevLineEmptyOrIndented || endLine || (notEmpty && !currLineEmptyOrIndented)) { + effectiveLineNumber -= 1; + } else if (nextLineEmptyOrIndented || (notEmpty && currLineEmptyOrIndented)) { + effectiveLineNumber += 1; + } } else if ((lineNumber < model.getLineCount()) && !isFolded(lineNumber + 1)) { effectiveLineNumber += 1; } else if (column * fontInfo.spaceWidth < 22) { diff --git a/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts index c1783a39f11..a98da2b0e0d 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts +++ b/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts index 641f1491d6f..664a36b2dca 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts +++ b/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyCodeChord } from 'vs/base/common/keybindings'; import { KeyCode } from 'vs/base/common/keyCodes'; import { OperatingSystem } from 'vs/base/common/platform'; @@ -101,4 +101,3 @@ function createCodeActionKeybinding(keycode: KeyCode, command: string, commandAr null, false); } - diff --git a/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts index 71d6df33c0b..5946fb24f50 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts +++ b/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { promiseWithResolvers } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { assertType } from 'vs/base/common/types'; diff --git a/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts b/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts index e37a442f643..32956ec08da 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts @@ -6,7 +6,7 @@ import { AsyncIterableObject } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Color, RGBA } from 'vs/base/common/color'; -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; @@ -16,11 +16,12 @@ import { getColorPresentations, getColors } from 'vs/editor/contrib/colorPicker/ import { ColorDetector } from 'vs/editor/contrib/colorPicker/browser/colorDetector'; import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/browser/colorPickerModel'; import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/browser/colorPickerWidget'; -import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import { Dimension } from 'vs/base/browser/dom'; +import * as nls from 'vs/nls'; export class ColorHover implements IHoverPart { @@ -50,6 +51,8 @@ export class ColorHoverParticipant implements IEditorHoverParticipant { + const renderedPart = renderHoverParts(this, this._editor, this._themeService, hoverParts, context); + if (!renderedPart) { + return new RenderedHoverParts([]); + } + this._colorPicker = renderedPart.colorPicker; + const renderedHoverPart: IRenderedHoverPart = { + hoverPart: renderedPart.hoverPart, + hoverElement: this._colorPicker.domNode, + dispose() { renderedPart.disposables.dispose(); } + }; + return new RenderedHoverParts([renderedHoverPart]); + } + + public getAccessibleContent(hoverPart: ColorHover): string { + return nls.localize('hoverAccessibilityColorParticipant', 'There is a color picker here.'); + } + + public handleResize(): void { + this._colorPicker?.layout(); + } + + public isColorPickerVisible(): boolean { + return !!this._colorPicker; } } @@ -146,7 +171,7 @@ export class StandaloneColorPickerParticipant { } } - public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: ColorHover[] | StandaloneColorPickerHover[]): IDisposable { + public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: StandaloneColorPickerHover[]): { disposables: IDisposable; hoverPart: StandaloneColorPickerHover; colorPicker: ColorPickerWidget } | undefined { return renderHoverParts(this, this._editor, this._themeService, hoverParts, context); } @@ -178,9 +203,9 @@ async function _createColorHover(participant: ColorHoverParticipant | Standalone } } -function renderHoverParts(participant: ColorHoverParticipant | StandaloneColorPickerParticipant, editor: ICodeEditor, themeService: IThemeService, hoverParts: ColorHover[] | StandaloneColorPickerHover[], context: IEditorHoverRenderContext) { +function renderHoverParts(participant: ColorHoverParticipant | StandaloneColorPickerParticipant, editor: ICodeEditor, themeService: IThemeService, hoverParts: T[], context: IEditorHoverRenderContext): { hoverPart: T; colorPicker: ColorPickerWidget; disposables: DisposableStore } | undefined { if (hoverParts.length === 0 || !editor.hasModel()) { - return Disposable.None; + return undefined; } if (context.setMinimumDimensions) { const minimumHeight = editor.getOption(EditorOption.lineHeight) + 8; @@ -191,13 +216,12 @@ function renderHoverParts(participant: ColorHoverParticipant | StandaloneColorPi const colorHover = hoverParts[0]; const editorModel = editor.getModel(); const model = colorHover.model; - const widget = disposables.add(new ColorPickerWidget(context.fragment, model, editor.getOption(EditorOption.pixelRatio), themeService, participant instanceof StandaloneColorPickerParticipant)); - context.setColorPicker(widget); + const colorPicker = disposables.add(new ColorPickerWidget(context.fragment, model, editor.getOption(EditorOption.pixelRatio), themeService, participant instanceof StandaloneColorPickerParticipant)); let editorUpdatedByColorPicker = false; let range = new Range(colorHover.range.startLineNumber, colorHover.range.startColumn, colorHover.range.endLineNumber, colorHover.range.endColumn); if (participant instanceof StandaloneColorPickerParticipant) { - const color = hoverParts[0].model.color; + const color = colorHover.model.color; participant.color = color; _updateColorPresentations(editorModel, model, color, range, colorHover); disposables.add(model.onColorFlushed((color: Color) => { @@ -221,7 +245,7 @@ function renderHoverParts(participant: ColorHoverParticipant | StandaloneColorPi editor.focus(); } })); - return disposables; + return { hoverPart: colorHover, colorPicker, disposables }; } function _updateEditorModel(editor: IActiveCodeEditor, range: Range, model: ColorPickerModel): Range { diff --git a/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts b/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts index 7688defca5f..b911fe5b0f5 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts @@ -468,6 +468,7 @@ export class InsertButton extends Disposable { export class ColorPickerWidget extends Widget implements IEditorHoverColorPickerWidget { private static readonly ID = 'editor.contrib.colorPickerWidget'; + private readonly _domNode: HTMLElement; body: ColorPickerBody; header: ColorPickerHeader; @@ -477,11 +478,11 @@ export class ColorPickerWidget extends Widget implements IEditorHoverColorPicker this._register(PixelRatio.getInstance(dom.getWindow(container)).onDidChange(() => this.layout())); - const element = $('.colorpicker-widget'); - container.appendChild(element); + this._domNode = $('.colorpicker-widget'); + container.appendChild(this._domNode); - this.header = this._register(new ColorPickerHeader(element, this.model, themeService, standaloneColorPicker)); - this.body = this._register(new ColorPickerBody(element, this.model, this.pixelRatio, standaloneColorPicker)); + this.header = this._register(new ColorPickerHeader(this._domNode, this.model, themeService, standaloneColorPicker)); + this.body = this._register(new ColorPickerBody(this._domNode, this.model, this.pixelRatio, standaloneColorPicker)); } getId(): string { @@ -491,4 +492,8 @@ export class ColorPickerWidget extends Widget implements IEditorHoverColorPicker layout(): void { this.body.layout(); } + + get domNode(): HTMLElement { + return this._domNode; + } } diff --git a/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerWidget.ts b/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerWidget.ts index 4c8cf865269..424448611b1 100644 --- a/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerWidget.ts +++ b/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerWidget.ts @@ -12,7 +12,7 @@ import { StandaloneColorPickerHover, StandaloneColorPickerParticipant } from 'vs import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EditorHoverStatusBar } from 'vs/editor/contrib/hover/browser/contentHoverStatusBar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ColorPickerWidget, InsertButton } from 'vs/editor/contrib/colorPicker/browser/colorPickerWidget'; +import { InsertButton } from 'vs/editor/contrib/colorPicker/browser/colorPickerWidget'; import { Emitter } from 'vs/base/common/event'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IColorInformation } from 'vs/editor/common/languages'; @@ -215,42 +215,42 @@ export class StandaloneColorPickerWidget extends Disposable implements IContentW private _render(colorHover: StandaloneColorPickerHover, foundInEditor: boolean) { const fragment = document.createDocumentFragment(); const statusBar = this._register(new EditorHoverStatusBar(this._keybindingService)); - let colorPickerWidget: ColorPickerWidget | undefined; const context: IEditorHoverRenderContext = { fragment, statusBar, - setColorPicker: (widget: ColorPickerWidget) => colorPickerWidget = widget, onContentsChanged: () => { }, hide: () => this.hide() }; this._colorHover = colorHover; - this._register(this._standaloneColorPickerParticipant.renderHoverParts(context, [colorHover])); - if (colorPickerWidget === undefined) { + const renderedHoverPart = this._standaloneColorPickerParticipant.renderHoverParts(context, [colorHover]); + if (!renderedHoverPart) { return; } + this._register(renderedHoverPart.disposables); + const colorPicker = renderedHoverPart.colorPicker; this._body.classList.add('standalone-colorpicker-body'); this._body.style.maxHeight = Math.max(this._editor.getLayoutInfo().height / 4, 250) + 'px'; this._body.style.maxWidth = Math.max(this._editor.getLayoutInfo().width * 0.66, 500) + 'px'; this._body.tabIndex = 0; this._body.appendChild(fragment); - colorPickerWidget.layout(); + colorPicker.layout(); - const colorPickerBody = colorPickerWidget.body; + const colorPickerBody = colorPicker.body; const saturationBoxWidth = colorPickerBody.saturationBox.domNode.clientWidth; const widthOfOriginalColorBox = colorPickerBody.domNode.clientWidth - saturationBoxWidth - CLOSE_BUTTON_WIDTH - PADDING; - const enterButton: InsertButton | null = colorPickerWidget.body.enterButton; + const enterButton: InsertButton | null = colorPicker.body.enterButton; enterButton?.onClicked(() => { this.updateEditor(); this.hide(); }); - const colorPickerHeader = colorPickerWidget.header; + const colorPickerHeader = colorPicker.header; const pickedColorNode = colorPickerHeader.pickedColorNode; pickedColorNode.style.width = saturationBoxWidth + PADDING + 'px'; const originalColorNode = colorPickerHeader.originalColorNode; originalColorNode.style.width = widthOfOriginalColorBox + 'px'; - const closeButton = colorPickerWidget.header.closeButton; + const closeButton = colorPicker.header.closeButton; closeButton?.onClicked(() => { this.hide(); }); diff --git a/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts b/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts index ff394a835bb..f40f7b1e252 100644 --- a/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts +++ b/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Selection } from 'vs/editor/common/core/selection'; diff --git a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts index 95609201da3..ddfaabbf496 100644 --- a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts @@ -227,7 +227,7 @@ export class ContextMenuController implements IEditorContribution { // Show menu this._contextMenuIsBeingShownCount++; this._contextMenuService.showContextMenu({ - domForShadowRoot: useShadowDOM ? this._editor.getDomNode() : undefined, + domForShadowRoot: useShadowDOM ? this._editor.getOverflowWidgetsDomNode() ?? this._editor.getDomNode() : undefined, getAnchor: () => anchor, diff --git a/src/vs/editor/contrib/cursorUndo/test/browser/cursorUndo.test.ts b/src/vs/editor/contrib/cursorUndo/test/browser/cursorUndo.test.ts index 3cecf3ee8eb..d0e3423a47b 100644 --- a/src/vs/editor/contrib/cursorUndo/test/browser/cursorUndo.test.ts +++ b/src/vs/editor/contrib/cursorUndo/test/browser/cursorUndo.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreEditingCommands, CoreNavigationCommands } from 'vs/editor/browser/coreCommands'; import { Selection } from 'vs/editor/common/core/selection'; diff --git a/src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts b/src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts index 194b8ee4f16..185d30db3e6 100644 --- a/src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts +++ b/src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index 87cddb00a8d..ad79eb6a01f 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -5,11 +5,11 @@ import { addDisposableListener, getActiveDocument } from 'vs/base/browser/dom'; import { coalesce } from 'vs/base/common/arrays'; -import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancelablePromise, createCancelablePromise, DeferredPromise, raceCancellation } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { UriList, VSDataTransfer, createStringDataTransferItem, matchesMimeType } from 'vs/base/common/dataTransfer'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import * as platform from 'vs/base/common/platform'; import { generateUuid } from 'vs/base/common/uuid'; @@ -36,6 +36,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { PostEditWidgetManager } from './postEditWidget'; +import { CancellationError, isCancellationError } from 'vs/base/common/errors'; export const changePasteTypeCommandId = 'editor.changePasteType'; @@ -54,6 +55,12 @@ type PasteEditWithProvider = DocumentPasteEdit & { provider: DocumentPasteEditProvider; }; + +interface DocumentPasteWithProviderEditsSession { + edits: readonly PasteEditWithProvider[]; + dispose(): void; +} + type PastePreference = | HierarchicalKind | { providerId: string }; @@ -299,17 +306,28 @@ export class CopyPasteController extends Disposable implements IEditorContributi } private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, clipboardEvent: ClipboardEvent): void { - const p = createCancelablePromise(async (token) => { + const editor = this._editor; + if (!editor.hasModel()) { + return; + } + + const editorStateCts = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined); + + const p = createCancelablePromise(async (pToken) => { const editor = this._editor; if (!editor.hasModel()) { return; } const model = editor.getModel(); - const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token); + const disposables = new DisposableStore(); + const cts = disposables.add(new CancellationTokenSource(pToken)); + disposables.add(editorStateCts.token.onCancellationRequested(() => cts.cancel())); + + const token = cts.token; try { - await this.mergeInDataFromCopy(dataTransfer, metadata, tokenSource.token); - if (tokenSource.token.isCancellationRequested) { + await this.mergeInDataFromCopy(dataTransfer, metadata, token); + if (token.isCancellationRequested) { return; } @@ -317,43 +335,75 @@ export class CopyPasteController extends Disposable implements IEditorContributi if (!supportedProviders.length || (supportedProviders.length === 1 && supportedProviders[0] instanceof DefaultTextPasteOrDropEditProvider) // Only our default text provider is active ) { - return this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token, clipboardEvent); + return this.applyDefaultPasteHandler(dataTransfer, metadata, token, clipboardEvent); } const context: DocumentPasteContext = { triggerKind: DocumentPasteTriggerKind.Automatic, }; - const providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token); - if (tokenSource.token.isCancellationRequested) { + const editSession = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, token); + disposables.add(editSession); + if (token.isCancellationRequested) { return; } // If the only edit returned is our default text edit, use the default paste handler - if (providerEdits.length === 1 && providerEdits[0].provider instanceof DefaultTextPasteOrDropEditProvider) { - return this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token, clipboardEvent); + if (editSession.edits.length === 1 && editSession.edits[0].provider instanceof DefaultTextPasteOrDropEditProvider) { + return this.applyDefaultPasteHandler(dataTransfer, metadata, token, clipboardEvent); } - if (providerEdits.length) { + if (editSession.edits.length) { const canShowWidget = editor.getOption(EditorOption.pasteAs).showPasteSelector === 'afterPaste'; - return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: 0, allEdits: providerEdits }, canShowWidget, async (edit, token) => { - const resolved = await edit.provider.resolveDocumentPasteEdit?.(edit, token); - if (resolved) { - edit.additionalEdit = resolved.additionalEdit; - } - return edit; - }, tokenSource.token); + return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: 0, allEdits: editSession.edits }, canShowWidget, (edit, token) => { + return new Promise((resolve, reject) => { + (async () => { + try { + const resolveP = edit.provider.resolveDocumentPasteEdit?.(edit, token); + const showP = new DeferredPromise(); + const resolved = resolveP && await this._pasteProgressManager.showWhile(selections[0].getEndPosition(), localize('resolveProcess', "Resolving paste edit. Click to cancel"), Promise.race([showP.p, resolveP]), { + cancel: () => { + showP.cancel(); + return reject(new CancellationError()); + } + }, 0); + if (resolved) { + edit.additionalEdit = resolved.additionalEdit; + } + return resolve(edit); + } catch (err) { + return reject(err); + } + })(); + }); + }, token); } - await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token, clipboardEvent); + await this.applyDefaultPasteHandler(dataTransfer, metadata, token, clipboardEvent); } finally { - tokenSource.dispose(); + disposables.dispose(); if (this._currentPasteOperation === p) { this._currentPasteOperation = undefined; } } }); - this._pasteProgressManager.showWhile(selections[0].getEndPosition(), localize('pasteIntoEditorProgress', "Running paste handlers. Click to cancel"), p); + this._pasteProgressManager.showWhile(selections[0].getEndPosition(), localize('pasteIntoEditorProgress', "Running paste handlers. Click to cancel and do basic paste"), p, { + cancel: async () => { + try { + p.cancel(); + + if (editorStateCts.token.isCancellationRequested) { + return; + } + + await this.applyDefaultPasteHandler(dataTransfer, metadata, editorStateCts.token, clipboardEvent); + } finally { + editorStateCts.dispose(); + } + } + }).then(() => { + editorStateCts.dispose(); + }); this._currentPasteOperation = p; } @@ -365,7 +415,8 @@ export class CopyPasteController extends Disposable implements IEditorContributi } const model = editor.getModel(); - const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token); + const disposables = new DisposableStore(); + const tokenSource = disposables.add(new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token)); try { await this.mergeInDataFromCopy(dataTransfer, metadata, tokenSource.token); if (tokenSource.token.isCancellationRequested) { @@ -383,23 +434,26 @@ export class CopyPasteController extends Disposable implements IEditorContributi triggerKind: DocumentPasteTriggerKind.PasteAs, only: preference && preference instanceof HierarchicalKind ? preference : undefined, }; - let providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token); + let editSession = disposables.add(await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token)); if (tokenSource.token.isCancellationRequested) { return; } // Filter out any edits that don't match the requested kind if (preference) { - providerEdits = providerEdits.filter(edit => { - if (preference instanceof HierarchicalKind) { - return preference.contains(edit.kind); - } else { - return preference.providerId === edit.provider.id; - } - }); + editSession = { + edits: editSession.edits.filter(edit => { + if (preference instanceof HierarchicalKind) { + return preference.contains(edit.kind); + } else { + return preference.providerId === edit.provider.id; + } + }), + dispose: editSession.dispose + }; } - if (!providerEdits.length) { + if (!editSession.edits.length) { if (context.only) { this.showPasteAsNoEditMessage(selections, context.only); } @@ -408,10 +462,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi let pickedEdit: DocumentPasteEdit | undefined; if (preference) { - pickedEdit = providerEdits.at(0); + pickedEdit = editSession.edits.at(0); } else { const selected = await this._quickInputService.pick( - providerEdits.map((edit): IQuickPickItem & { edit: DocumentPasteEdit } => ({ + editSession.edits.map((edit): IQuickPickItem & { edit: DocumentPasteEdit } => ({ label: edit.title, description: edit.kind?.value, edit, @@ -428,7 +482,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, selections, pickedEdit); await this._bulkEditService.apply(combinedWorkspaceEdit, { editor: this._editor }); } finally { - tokenSource.dispose(); + disposables.dispose(); if (this._currentPasteOperation === p) { this._currentPasteOperation = undefined; } @@ -499,23 +553,32 @@ export class CopyPasteController extends Disposable implements IEditorContributi } } - private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], context: DocumentPasteContext, token: CancellationToken): Promise { + private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], context: DocumentPasteContext, token: CancellationToken): Promise { + const disposables = new DisposableStore(); + const results = await raceCancellation( Promise.all(providers.map(async provider => { try { const edits = await provider.provideDocumentPasteEdits?.(model, selections, dataTransfer, context, token); - // TODO: dispose of edits + if (edits) { + disposables.add(edits); + } return edits?.edits?.map(edit => ({ ...edit, provider })); } catch (err) { - console.error(err); + if (!isCancellationError(err)) { + console.error(err); + } + return undefined; } - return undefined; })), token); const edits = coalesce(results ?? []).flat().filter(edit => { return !context.only || context.only.contains(edit.kind); }); - return sortEditsByYieldTo(edits); + return { + edits: sortEditsByYieldTo(edits), + dispose: () => disposables.dispose() + }; } private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken, clipboardEvent: ClipboardEvent) { diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts index 006713618a6..e44d9341c9b 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts @@ -14,7 +14,7 @@ import { relativePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; -import { DocumentDropEdit, DocumentDropEditProvider, DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession, DocumentPasteTriggerKind } from 'vs/editor/common/languages'; +import { DocumentDropEditProvider, DocumentDropEditsSession, DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession, DocumentPasteTriggerKind } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { localize } from 'vs/nls'; @@ -34,14 +34,20 @@ abstract class SimplePasteAndDropProvider implements DocumentDropEditProvider, D } return { + edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }], dispose() { }, - edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }] }; } - async provideDocumentDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + async provideDocumentDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { const edit = await this.getEdit(dataTransfer, token); - return edit ? [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }] : undefined; + if (!edit) { + return; + } + return { + edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }], + dispose() { }, + }; } protected abstract getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise; diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts index 20adc65da11..082b7447881 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts @@ -7,7 +7,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async'; import { VSDataTransfer, matchesMimeType } from 'vs/base/common/dataTransfer'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { toExternalVSDataTransfer } from 'vs/editor/browser/dnd'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -84,8 +84,9 @@ export class DropIntoEditorController extends Disposable implements IEditorContr editor.setPosition(position); const p = createCancelablePromise(async (token) => { - const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value, undefined, token); + const disposables = new DisposableStore(); + const tokenSource = disposables.add(new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value, undefined, token)); try { const ourDataTransfer = await this.extractDataTransferData(dragEvent); if (ourDataTransfer.size === 0 || tokenSource.token.isCancellationRequested) { @@ -107,34 +108,39 @@ export class DropIntoEditorController extends Disposable implements IEditorContr return provider.dropMimeTypes.some(mime => ourDataTransfer.matches(mime)); }); - const edits = await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource); + const editSession = disposables.add(await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource)); if (tokenSource.token.isCancellationRequested) { return; } - if (edits.length) { - const activeEditIndex = this.getInitialActiveEditIndex(model, edits); + if (editSession.edits.length) { + const activeEditIndex = this.getInitialActiveEditIndex(model, editSession.edits); const canShowWidget = editor.getOption(EditorOption.dropIntoEditor).showDropSelector === 'afterDrop'; // Pass in the parent token here as it tracks cancelling the entire drop operation - await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: edits }, canShowWidget, async edit => edit, token); + await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: editSession.edits }, canShowWidget, async edit => edit, token); } } finally { - tokenSource.dispose(); + disposables.dispose(); if (this._currentOperation === p) { this._currentOperation = undefined; } } }); - this._dropProgressManager.showWhile(position, localize('dropIntoEditorProgress', "Running drop handlers. Click to cancel"), p); + this._dropProgressManager.showWhile(position, localize('dropIntoEditorProgress', "Running drop handlers. Click to cancel"), p, { cancel: () => p.cancel() }); this._currentOperation = p; } private async getDropEdits(providers: readonly DocumentDropEditProvider[], model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, tokenSource: EditorStateCancellationTokenSource) { + const disposables = new DisposableStore(); + const results = await raceCancellation(Promise.all(providers.map(async provider => { try { const edits = await provider.provideDocumentDropEdits(model, position, dataTransfer, tokenSource.token); - return edits?.map(edit => ({ ...edit, providerId: provider.id })); + if (edits) { + disposables.add(edits); + } + return edits?.edits.map(edit => ({ ...edit, providerId: provider.id })); } catch (err) { console.error(err); } @@ -142,7 +148,10 @@ export class DropIntoEditorController extends Disposable implements IEditorContr })), tokenSource.token); const edits = coalesce(results ?? []).flat(); - return sortEditsByYieldTo(edits); + return { + edits: sortEditsByYieldTo(edits), + dispose: () => disposables.dispose() + }; } private getInitialActiveEditIndex(model: ITextModel, edits: ReadonlyArray) { diff --git a/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts b/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts index ee0b8e56b61..ec61f04a463 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DocumentDropEdit } from 'vs/editor/common/languages'; diff --git a/src/vs/editor/contrib/editorState/test/browser/editorState.test.ts b/src/vs/editor/contrib/editorState/test/browser/editorState.test.ts index cc2a8d0f93e..239da13d225 100644 --- a/src/vs/editor/contrib/editorState/test/browser/editorState.test.ts +++ b/src/vs/editor/contrib/editorState/test/browser/editorState.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index 303a9a58aaa..9645d4b54ba 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -410,9 +410,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL } // remove previous content - if (this._matchesCount.firstChild) { - this._matchesCount.removeChild(this._matchesCount.firstChild); - } + this._matchesCount.firstChild?.remove(); let label: string; if (this._state.matchesCount > 0) { @@ -1345,7 +1343,7 @@ export class SimpleButton extends Widget { this._domNode.className = className; this._domNode.setAttribute('role', 'button'); this._domNode.setAttribute('aria-label', this._opts.label); - this._register(hoverService.setupUpdatableHover(opts.hoverDelegate ?? getDefaultHoverDelegate('element'), this._domNode, this._opts.label)); + this._register(hoverService.setupManagedHover(opts.hoverDelegate ?? getDefaultHoverDelegate('element'), this._domNode, this._opts.label)); this.onclick(this._domNode, (e) => { this._opts.onTrigger(); diff --git a/src/vs/editor/contrib/find/test/browser/find.test.ts b/src/vs/editor/contrib/find/test/browser/find.test.ts index 466c39baf1e..580ea739b76 100644 --- a/src/vs/editor/contrib/find/test/browser/find.test.ts +++ b/src/vs/editor/contrib/find/test/browser/find.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/contrib/find/test/browser/findController.test.ts b/src/vs/editor/contrib/find/test/browser/findController.test.ts index 9bdb1cb7786..49fef95baf8 100644 --- a/src/vs/editor/contrib/find/test/browser/findController.test.ts +++ b/src/vs/editor/contrib/find/test/browser/findController.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Delayer } from 'vs/base/common/async'; import * as platform from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/contrib/find/test/browser/findModel.test.ts b/src/vs/editor/contrib/find/test/browser/findModel.test.ts index 7d1ae5f5bd5..8cc753388b9 100644 --- a/src/vs/editor/contrib/find/test/browser/findModel.test.ts +++ b/src/vs/editor/contrib/find/test/browser/findModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreNavigationCommands } from 'vs/editor/browser/coreCommands'; diff --git a/src/vs/editor/contrib/find/test/browser/replacePattern.test.ts b/src/vs/editor/contrib/find/test/browser/replacePattern.test.ts index cc9e76c93b5..1f534bbdae3 100644 --- a/src/vs/editor/contrib/find/test/browser/replacePattern.test.ts +++ b/src/vs/editor/contrib/find/test/browser/replacePattern.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { parseReplaceString, ReplacePattern, ReplacePiece } from 'vs/editor/contrib/find/browser/replacePattern'; diff --git a/src/vs/editor/contrib/folding/browser/folding.css b/src/vs/editor/contrib/folding/browser/folding.css index f973d5f7a30..5f7ab05db78 100644 --- a/src/vs/editor/contrib/folding/browser/folding.css +++ b/src/vs/editor/contrib/folding/browser/folding.css @@ -31,7 +31,7 @@ } .monaco-editor .inline-folded:after { - color: grey; + color: var(--vscode-editor-foldPlaceholderForeground); margin: 0.1em 0.2em 0 0.2em; content: "\22EF"; /* ellipses unicode character */ display: inline; @@ -49,4 +49,3 @@ .monaco-editor .cldr.codicon.codicon-folding-manual-collapsed { color: var(--vscode-editorGutter-foldingControlForeground) !important; } - diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index 993c8cf0827..25988ac7370 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -809,6 +809,30 @@ class FoldRecursivelyAction extends FoldingAction { } } + +class ToggleFoldRecursivelyAction extends FoldingAction { + + constructor() { + super({ + id: 'editor.toggleFoldRecursively', + label: nls.localize('toggleFoldRecursivelyAction.label', "Toggle Fold Recursively"), + alias: 'Toggle Fold Recursively', + precondition: CONTEXT_FOLDING_ENABLED, + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL), + weight: KeybindingWeight.EditorContrib + } + }); + } + + invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void { + const selectedLines = this.getSelectedLines(editor); + toggleCollapseState(foldingModel, Number.MAX_VALUE, selectedLines); + } +} + + class FoldAllBlockCommentsAction extends FoldingAction { constructor() { @@ -1189,6 +1213,7 @@ registerEditorAction(UnfoldAction); registerEditorAction(UnFoldRecursivelyAction); registerEditorAction(FoldAction); registerEditorAction(FoldRecursivelyAction); +registerEditorAction(ToggleFoldRecursivelyAction); registerEditorAction(FoldAllAction); registerEditorAction(UnfoldAllAction); registerEditorAction(FoldAllBlockCommentsAction); diff --git a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts index 03a9b4a402c..2350f9aad34 100644 --- a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts @@ -15,7 +15,8 @@ import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; const foldBackground = registerColor('editor.foldBackground', { light: transparent(editorSelectionBackground, 0.3), dark: transparent(editorSelectionBackground, 0.3), hcDark: null, hcLight: null }, localize('foldBackgroundBackground', "Background color behind folded ranges. The color must not be opaque so as not to hide underlying decorations."), true); -registerColor('editorGutter.foldingControlForeground', { dark: iconForeground, light: iconForeground, hcDark: iconForeground, hcLight: iconForeground }, localize('editorGutter.foldingControlForeground', 'Color of the folding control in the editor gutter.')); +registerColor('editor.foldPlaceholderForeground', { light: '#808080', dark: '#808080', hcDark: null, hcLight: null }, localize('collapsedTextColor', "Color of the collapsed text after the first line of a folded range.")); +registerColor('editorGutter.foldingControlForeground', iconForeground, localize('editorGutter.foldingControlForeground', 'Color of the folding control in the editor gutter.')); export const foldingExpandedIcon = registerIcon('folding-expanded', Codicon.chevronDown, localize('foldingExpandedIcon', 'Icon for expanded ranges in the editor glyph margin.')); export const foldingCollapsedIcon = registerIcon('folding-collapsed', Codicon.chevronRight, localize('foldingCollapsedIcon', 'Icon for collapsed ranges in the editor glyph margin.')); diff --git a/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts b/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts index 3cef0c2a56e..1ea615a5bd9 100644 --- a/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts index 912cf515d1a..a1a6dbe1dc1 100644 --- a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { FoldingMarkers } from 'vs/editor/common/languages/languageConfiguration'; import { MAX_FOLDING_REGIONS, FoldRange, FoldingRegions, FoldSource } from 'vs/editor/contrib/folding/browser/foldingRanges'; diff --git a/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts b/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts index 71281efa963..6d57dec4e78 100644 --- a/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IRange } from 'vs/editor/common/core/range'; import { FoldingModel } from 'vs/editor/contrib/folding/browser/foldingModel'; import { HiddenRangeModel } from 'vs/editor/contrib/folding/browser/hiddenRangeModel'; diff --git a/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts b/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts index afee83dff29..25db0e6d15d 100644 --- a/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { computeRanges } from 'vs/editor/contrib/folding/browser/indentRangeProvider'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; diff --git a/src/vs/editor/contrib/folding/test/browser/indentRangeProvider.test.ts b/src/vs/editor/contrib/folding/test/browser/indentRangeProvider.test.ts index 0a1c3220e64..837dd6c0bc4 100644 --- a/src/vs/editor/contrib/folding/test/browser/indentRangeProvider.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/indentRangeProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { FoldingMarkers } from 'vs/editor/common/languages/languageConfiguration'; import { computeRanges } from 'vs/editor/contrib/folding/browser/indentRangeProvider'; diff --git a/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts b/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts index 3551f446606..3b2bdb0648e 100644 --- a/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModel } from 'vs/editor/common/model'; import { FoldingContext, FoldingRange, FoldingRangeProvider, ProviderResult } from 'vs/editor/common/languages'; diff --git a/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts index 24ffac212bd..00f716bb45f 100644 --- a/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts @@ -410,4 +410,4 @@ const editorMarkerNavigationWarningHeader = registerColor('editorMarkerNavigatio const editorMarkerNavigationInfo = registerColor('editorMarkerNavigationInfo.background', { dark: infoDefault, light: infoDefault, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('editorMarkerNavigationInfo', 'Editor marker navigation widget info color.')); const editorMarkerNavigationInfoHeader = registerColor('editorMarkerNavigationInfo.headerBackground', { dark: transparent(editorMarkerNavigationInfo, .1), light: transparent(editorMarkerNavigationInfo, .1), hcDark: null, hcLight: null }, nls.localize('editorMarkerNavigationInfoHeaderBackground', 'Editor marker navigation widget info heading background.')); -const editorMarkerNavigationBackground = registerColor('editorMarkerNavigation.background', { dark: editorBackground, light: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, nls.localize('editorMarkerNavigationBackground', 'Editor marker navigation widget background.')); +const editorMarkerNavigationBackground = registerColor('editorMarkerNavigation.background', editorBackground, nls.localize('editorMarkerNavigationBackground', 'Editor marker navigation widget background.')); diff --git a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts index b80e75d47ec..0941671756d 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts @@ -390,7 +390,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { this._onDidSelectReference.fire({ element, kind, source: 'tree' }); } }; - this._tree.onDidOpen(e => { + this._disposables.add(this._tree.onDidOpen(e => { if (e.sideBySide) { onEvent(e.element, 'side'); } else if (e.editorOptions.pinned) { @@ -398,7 +398,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { } else { onEvent(e.element, 'show'); } - }); + })); dom.hide(this._treeContainer); } diff --git a/src/vs/editor/contrib/gotoSymbol/test/browser/referencesModel.test.ts b/src/vs/editor/contrib/gotoSymbol/test/browser/referencesModel.test.ts index d1195449779..a547f9450e1 100644 --- a/src/vs/editor/contrib/gotoSymbol/test/browser/referencesModel.test.ts +++ b/src/vs/editor/contrib/gotoSymbol/test/browser/referencesModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/contrib/hover/browser/contentHoverController.ts b/src/vs/editor/contrib/hover/browser/contentHoverController.ts index 81ad7b790ed..d79984f24c1 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverController.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverController.ts @@ -5,35 +5,30 @@ import * as dom from 'vs/base/browser/dom'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { TokenizationRegistry } from 'vs/editor/common/languages'; import { HoverOperation, HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; -import { HoverAnchor, HoverParticipantRegistry, HoverRangeAnchor, IEditorHoverColorPickerWidget, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IHoverWidget } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverParticipantRegistry, HoverRangeAnchor, IEditorHoverContext, IEditorHoverParticipant, IHoverPart, IHoverWidget } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; -import { InlayHintsHover } from 'vs/editor/contrib/inlayHints/browser/inlayHintsHover'; import { HoverVerbosityAction } from 'vs/editor/common/standalone/standaloneEnums'; import { ContentHoverWidget } from 'vs/editor/contrib/hover/browser/contentHoverWidget'; import { ContentHoverComputer } from 'vs/editor/contrib/hover/browser/contentHoverComputer'; -import { ContentHoverVisibleData, HoverResult } from 'vs/editor/contrib/hover/browser/contentHoverTypes'; -import { EditorHoverStatusBar } from 'vs/editor/contrib/hover/browser/contentHoverStatusBar'; +import { HoverResult } from 'vs/editor/contrib/hover/browser/contentHoverTypes'; import { Emitter } from 'vs/base/common/event'; +import { RenderedContentHover } from 'vs/editor/contrib/hover/browser/contentHoverRendered'; export class ContentHoverController extends Disposable implements IHoverWidget { private _currentResult: HoverResult | null = null; + private _renderedContentHover: RenderedContentHover | undefined; private readonly _computer: ContentHoverComputer; - private readonly _widget: ContentHoverWidget; + private readonly _contentHoverWidget: ContentHoverWidget; private readonly _participants: IEditorHoverParticipant[]; - // TODO@aiday-mar make array of participants, dispatch between them - private readonly _markdownHoverParticipant: MarkdownHoverParticipant | undefined; private readonly _hoverOperation: HoverOperation; private readonly _onContentsChanged = this._register(new Emitter()); @@ -45,23 +40,27 @@ export class ContentHoverController extends Disposable implements IHoverWidget { @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { super(); + this._contentHoverWidget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor)); + this._participants = this._initializeHoverParticipants(); + this._computer = new ContentHoverComputer(this._editor, this._participants); + this._hoverOperation = this._register(new HoverOperation(this._editor, this._computer)); + this._registerListeners(); + } - this._widget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor)); - - // Instantiate participants and sort them by `hoverOrdinal` which is relevant for rendering order. - this._participants = []; + private _initializeHoverParticipants(): IEditorHoverParticipant[] { + const participants: IEditorHoverParticipant[] = []; for (const participant of HoverParticipantRegistry.getAll()) { const participantInstance = this._instantiationService.createInstance(participant, this._editor); - if (participantInstance instanceof MarkdownHoverParticipant && !(participantInstance instanceof InlayHintsHover)) { - this._markdownHoverParticipant = participantInstance; - } - this._participants.push(participantInstance); + participants.push(participantInstance); } - this._participants.sort((p1, p2) => p1.hoverOrdinal - p2.hoverOrdinal); - - this._computer = new ContentHoverComputer(this._editor, this._participants); - this._hoverOperation = this._register(new HoverOperation(this._editor, this._computer)); + participants.sort((p1, p2) => p1.hoverOrdinal - p2.hoverOrdinal); + this._register(this._contentHoverWidget.onDidResize(() => { + this._participants.forEach(participant => participant.handleResize?.()); + })); + return participants; + } + private _registerListeners(): void { this._register(this._hoverOperation.onResult((result) => { if (!this._computer.anchor) { // invalid state, ignore result @@ -70,13 +69,13 @@ export class ContentHoverController extends Disposable implements IHoverWidget { const messages = (result.hasLoadingMessage ? this._addLoadingMessage(result.value) : result.value); this._withResult(new HoverResult(this._computer.anchor, messages, result.isComplete)); })); - this._register(dom.addStandardDisposableListener(this._widget.getDomNode(), 'keydown', (e) => { + this._register(dom.addStandardDisposableListener(this._contentHoverWidget.getDomNode(), 'keydown', (e) => { if (e.equals(KeyCode.Escape)) { this.hide(); } })); this._register(TokenizationRegistry.onDidChange(() => { - if (this._widget.position && this._currentResult) { + if (this._contentHoverWidget.position && this._currentResult) { this._setCurrentResult(this._currentResult); // render again } })); @@ -92,61 +91,52 @@ export class ContentHoverController extends Disposable implements IHoverWidget { focus: boolean, mouseEvent: IEditorMouseEvent | null ): boolean { - - if (!this._widget.position || !this._currentResult) { - // The hover is not visible + const contentHoverIsVisible = this._contentHoverWidget.position && this._currentResult; + if (!contentHoverIsVisible) { if (anchor) { this._startHoverOperationIfNecessary(anchor, mode, source, focus, false); return true; } return false; } - - // The hover is currently visible const isHoverSticky = this._editor.getOption(EditorOption.hover).sticky; - const isGettingCloser = ( - isHoverSticky - && mouseEvent - && this._widget.isMouseGettingCloser(mouseEvent.event.posx, mouseEvent.event.posy) - ); - - if (isGettingCloser) { - // The mouse is getting closer to the hover, so we will keep the hover untouched - // But we will kick off a hover update at the new anchor, insisting on keeping the hover visible. + const isMouseGettingCloser = mouseEvent && this._contentHoverWidget.isMouseGettingCloser(mouseEvent.event.posx, mouseEvent.event.posy); + const isHoverStickyAndIsMouseGettingCloser = isHoverSticky && isMouseGettingCloser; + // The mouse is getting closer to the hover, so we will keep the hover untouched + // But we will kick off a hover update at the new anchor, insisting on keeping the hover visible. + if (isHoverStickyAndIsMouseGettingCloser) { if (anchor) { this._startHoverOperationIfNecessary(anchor, mode, source, focus, true); } return true; } - + // If mouse is not getting closer and anchor not defined, hide the hover if (!anchor) { this._setCurrentResult(null); return false; } - - if (anchor && this._currentResult.anchor.equals(anchor)) { - // The widget is currently showing results for the exact same anchor, so no update is needed + // If mouse if not getting closer and anchor is defined, and the new anchor is the same as the previous anchor + const currentAnchorEqualsPreviousAnchor = this._currentResult!.anchor.equals(anchor); + if (currentAnchorEqualsPreviousAnchor) { return true; } - - if (!anchor.canAdoptVisibleHover(this._currentResult.anchor, this._widget.position)) { - // The new anchor is not compatible with the previous anchor + // If mouse if not getting closer and anchor is defined, and the new anchor is not compatible with the previous anchor + const currentAnchorCompatibleWithPreviousAnchor = anchor.canAdoptVisibleHover(this._currentResult!.anchor, this._contentHoverWidget.position); + if (!currentAnchorCompatibleWithPreviousAnchor) { this._setCurrentResult(null); this._startHoverOperationIfNecessary(anchor, mode, source, focus, false); return true; } - // We aren't getting any closer to the hover, so we will filter existing results // and keep those which also apply to the new anchor. - this._setCurrentResult(this._currentResult.filter(anchor)); + this._setCurrentResult(this._currentResult!.filter(anchor)); this._startHoverOperationIfNecessary(anchor, mode, source, focus, false); return true; } private _startHoverOperationIfNecessary(anchor: HoverAnchor, mode: HoverStartMode, source: HoverStartSource, focus: boolean, insistOnKeepingHoverVisible: boolean): void { - - if (this._computer.anchor && this._computer.anchor.equals(anchor)) { - // We have to start a hover operation at the exact same anchor as before, so no work is needed + const currentAnchorEqualToPreviousHover = this._computer.anchor && this._computer.anchor.equals(anchor); + if (currentAnchorEqualToPreviousHover) { return; } this._hoverOperation.cancel(); @@ -158,270 +148,213 @@ export class ContentHoverController extends Disposable implements IHoverWidget { } private _setCurrentResult(hoverResult: HoverResult | null): void { - - if (this._currentResult === hoverResult) { - // avoid updating the DOM to avoid resetting the user selection + let currentHoverResult = hoverResult; + const currentResultEqualToPreviousResult = this._currentResult === currentHoverResult; + if (currentResultEqualToPreviousResult) { return; } - if (hoverResult && hoverResult.messages.length === 0) { - hoverResult = null; + const currentHoverResultIsEmpty = currentHoverResult && currentHoverResult.hoverParts.length === 0; + if (currentHoverResultIsEmpty) { + currentHoverResult = null; } - this._currentResult = hoverResult; + this._currentResult = currentHoverResult; if (this._currentResult) { - this._renderMessages(this._currentResult.anchor, this._currentResult.messages); + this._showHover(this._currentResult); } else { - this._widget.hide(); + this._hideHover(); } } private _addLoadingMessage(result: IHoverPart[]): IHoverPart[] { - if (this._computer.anchor) { - for (const participant of this._participants) { - if (participant.createLoadingMessage) { - const loadingMessage = participant.createLoadingMessage(this._computer.anchor); - if (loadingMessage) { - return result.slice(0).concat([loadingMessage]); - } - } + if (!this._computer.anchor) { + return result; + } + for (const participant of this._participants) { + if (!participant.createLoadingMessage) { + continue; } + const loadingMessage = participant.createLoadingMessage(this._computer.anchor); + if (!loadingMessage) { + continue; + } + return result.slice(0).concat([loadingMessage]); } return result; } private _withResult(hoverResult: HoverResult): void { - if (this._widget.position && this._currentResult && this._currentResult.isComplete) { - // The hover is visible with a previous complete result. - - if (!hoverResult.isComplete) { - // Instead of rendering the new partial result, we wait for the result to be complete. - return; - } - - if (this._computer.insistOnKeepingHoverVisible && hoverResult.messages.length === 0) { - // The hover would now hide normally, so we'll keep the previous messages - return; - } + const previousHoverIsVisibleWithCompleteResult = this._contentHoverWidget.position && this._currentResult && this._currentResult.isComplete; + if (!previousHoverIsVisibleWithCompleteResult) { + this._setCurrentResult(hoverResult); } - - this._setCurrentResult(hoverResult); - } - - private _renderMessages(anchor: HoverAnchor, messages: IHoverPart[]): void { - const { showAtPosition, showAtSecondaryPosition, highlightRange } = ContentHoverController.computeHoverRanges(this._editor, anchor.range, messages); - - const disposables = new DisposableStore(); - const statusBar = disposables.add(new EditorHoverStatusBar(this._keybindingService)); - const fragment = document.createDocumentFragment(); - - let colorPicker: IEditorHoverColorPickerWidget | null = null; - const context: IEditorHoverRenderContext = { - fragment, - statusBar, - setColorPicker: (widget) => colorPicker = widget, - onContentsChanged: () => this._doOnContentsChanged(), - setMinimumDimensions: (dimensions: dom.Dimension) => this._widget.setMinimumDimensions(dimensions), - hide: () => this.hide() - }; - - for (const participant of this._participants) { - const hoverParts = messages.filter(msg => msg.owner === participant); - if (hoverParts.length > 0) { - disposables.add(participant.renderHoverParts(context, hoverParts)); - } + // The hover is visible with a previous complete result. + const isCurrentHoverResultComplete = hoverResult.isComplete; + if (!isCurrentHoverResultComplete) { + // Instead of rendering the new partial result, we wait for the result to be complete. + return; } - - const isBeforeContent = messages.some(m => m.isBeforeContent); - - if (statusBar.hasContent) { - fragment.appendChild(statusBar.hoverElement); + const currentHoverResultIsEmpty = hoverResult.hoverParts.length === 0; + const insistOnKeepingPreviousHoverVisible = this._computer.insistOnKeepingHoverVisible; + const shouldKeepPreviousHoverVisible = currentHoverResultIsEmpty && insistOnKeepingPreviousHoverVisible; + if (shouldKeepPreviousHoverVisible) { + // The hover would now hide normally, so we'll keep the previous messages + return; } + this._setCurrentResult(hoverResult); + } - if (fragment.hasChildNodes()) { - if (highlightRange) { - const highlightDecoration = this._editor.createDecorationsCollection(); - highlightDecoration.set([{ - range: highlightRange, - options: ContentHoverController._DECORATION_OPTIONS - }]); - disposables.add(toDisposable(() => { - highlightDecoration.clear(); - })); - } - - this._widget.showAt(fragment, new ContentHoverVisibleData( - anchor.initialMousePosX, - anchor.initialMousePosY, - colorPicker, - showAtPosition, - showAtSecondaryPosition, - this._editor.getOption(EditorOption.hover).above, - this._computer.shouldFocus, - this._computer.source, - isBeforeContent, - disposables - )); + private _showHover(hoverResult: HoverResult): void { + const context = this._getHoverContext(); + this._renderedContentHover = new RenderedContentHover(this._editor, hoverResult, this._participants, this._computer, context, this._keybindingService); + if (this._renderedContentHover.domNodeHasChildren) { + this._contentHoverWidget.show(this._renderedContentHover); } else { - disposables.dispose(); + this._renderedContentHover.dispose(); } } - private _doOnContentsChanged(): void { - this._onContentsChanged.fire(); - this._widget.onContentsChanged(); + private _hideHover(): void { + this._contentHoverWidget.hide(); } - private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ - description: 'content-hover-highlight', - className: 'hoverHighlight' - }); - - public static computeHoverRanges(editor: ICodeEditor, anchorRange: Range, messages: IHoverPart[]) { - - let startColumnBoundary = 1; - if (editor.hasModel()) { - // Ensure the range is on the current view line - const viewModel = editor._getViewModel(); - const coordinatesConverter = viewModel.coordinatesConverter; - const anchorViewRange = coordinatesConverter.convertModelRangeToViewRange(anchorRange); - const anchorViewRangeStart = new Position(anchorViewRange.startLineNumber, viewModel.getLineMinColumn(anchorViewRange.startLineNumber)); - startColumnBoundary = coordinatesConverter.convertViewPositionToModelPosition(anchorViewRangeStart).column; - } - - // The anchor range is always on a single line - const anchorLineNumber = anchorRange.startLineNumber; - let renderStartColumn = anchorRange.startColumn; - let highlightRange = messages[0].range; - let forceShowAtRange = null; - - for (const msg of messages) { - highlightRange = Range.plusRange(highlightRange, msg.range); - if (msg.range.startLineNumber === anchorLineNumber && msg.range.endLineNumber === anchorLineNumber) { - // this message has a range that is completely sitting on the line of the anchor - renderStartColumn = Math.max(Math.min(renderStartColumn, msg.range.startColumn), startColumnBoundary); - } - if (msg.forceShowAtRange) { - forceShowAtRange = msg.range; - } - } - - const showAtPosition = forceShowAtRange ? forceShowAtRange.getStartPosition() : new Position(anchorLineNumber, anchorRange.startColumn); - const showAtSecondaryPosition = forceShowAtRange ? forceShowAtRange.getStartPosition() : new Position(anchorLineNumber, renderStartColumn); - - return { - showAtPosition, - showAtSecondaryPosition, - highlightRange + private _getHoverContext(): IEditorHoverContext { + const hide = () => { + this.hide(); + }; + const onContentsChanged = () => { + this._onContentsChanged.fire(); + this._contentHoverWidget.onContentsChanged(); }; + const setMinimumDimensions = (dimensions: dom.Dimension) => { + this._contentHoverWidget.setMinimumDimensions(dimensions); + }; + return { hide, onContentsChanged, setMinimumDimensions }; } - public showsOrWillShow(mouseEvent: IEditorMouseEvent): boolean { - if (this._widget.isResizing) { + public showsOrWillShow(mouseEvent: IEditorMouseEvent): boolean { + const isContentWidgetResizing = this._contentHoverWidget.isResizing; + if (isContentWidgetResizing) { return true; } + const anchorCandidates: HoverAnchor[] = this._findHoverAnchorCandidates(mouseEvent); + const anchorCandidatesExist = anchorCandidates.length > 0; + if (!anchorCandidatesExist) { + return this._startShowingOrUpdateHover(null, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent); + } + const anchor = anchorCandidates[0]; + return this._startShowingOrUpdateHover(anchor, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent); + } + private _findHoverAnchorCandidates(mouseEvent: IEditorMouseEvent): HoverAnchor[] { const anchorCandidates: HoverAnchor[] = []; for (const participant of this._participants) { - if (participant.suggestHoverAnchor) { - const anchor = participant.suggestHoverAnchor(mouseEvent); - if (anchor) { - anchorCandidates.push(anchor); - } + if (!participant.suggestHoverAnchor) { + continue; + } + const anchor = participant.suggestHoverAnchor(mouseEvent); + if (!anchor) { + continue; } + anchorCandidates.push(anchor); } - const target = mouseEvent.target; - - if (target.type === MouseTargetType.CONTENT_TEXT) { - anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy)); - } - - if (target.type === MouseTargetType.CONTENT_EMPTY) { - const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2; - if ( - !target.detail.isAfterLines - && typeof target.detail.horizontalDistanceToText === 'number' - && target.detail.horizontalDistanceToText < epsilon - ) { + switch (target.type) { + case MouseTargetType.CONTENT_TEXT: { + anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy)); + break; + } + case MouseTargetType.CONTENT_EMPTY: { + const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2; // Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough + const mouseIsWithinLinesAndCloseToHover = !target.detail.isAfterLines + && typeof target.detail.horizontalDistanceToText === 'number' + && target.detail.horizontalDistanceToText < epsilon; + if (!mouseIsWithinLinesAndCloseToHover) { + break; + } anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy)); + break; } } - - if (anchorCandidates.length === 0) { - return this._startShowingOrUpdateHover(null, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent); - } - anchorCandidates.sort((a, b) => b.priority - a.priority); - return this._startShowingOrUpdateHover(anchorCandidates[0], HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent); + return anchorCandidates; } public startShowingAtRange(range: Range, mode: HoverStartMode, source: HoverStartSource, focus: boolean): void { this._startShowingOrUpdateHover(new HoverRangeAnchor(0, range, undefined, undefined), mode, source, focus, null); } - public async updateMarkdownHoverVerbosityLevel(action: HoverVerbosityAction, index?: number, focus?: boolean): Promise { - this._markdownHoverParticipant?.updateMarkdownHoverVerbosityLevel(action, index, focus); + public getWidgetContent(): string | undefined { + const node = this._contentHoverWidget.getDomNode(); + if (!node.textContent) { + return undefined; + } + return node.textContent; + } + + public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise { + this._renderedContentHover?.updateHoverVerbosityLevel(action, index, focus); } - public focusedMarkdownHoverIndex(): number { - return this._markdownHoverParticipant?.focusedMarkdownHoverIndex() ?? -1; + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + return this._renderedContentHover?.doesHoverAtIndexSupportVerbosityAction(index, action) ?? false; } - public markdownHoverContentAtIndex(index: number): string { - return this._markdownHoverParticipant?.markdownHoverContentAtIndex(index) ?? ''; + public getAccessibleWidgetContent(): string | undefined { + return this._renderedContentHover?.getAccessibleWidgetContent(); } - public doesMarkdownHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { - return this._markdownHoverParticipant?.doesMarkdownHoverAtIndexSupportVerbosityAction(index, action) ?? false; + public getAccessibleWidgetContentAtIndex(index: number): string | undefined { + return this._renderedContentHover?.getAccessibleWidgetContentAtIndex(index); } - public getWidgetContent(): string | undefined { - const node = this._widget.getDomNode(); - if (!node.textContent) { - return undefined; - } - return node.textContent; + public focusedHoverPartIndex(): number { + return this._renderedContentHover?.focusedHoverPartIndex ?? -1; } public containsNode(node: Node | null | undefined): boolean { - return (node ? this._widget.getDomNode().contains(node) : false); + return (node ? this._contentHoverWidget.getDomNode().contains(node) : false); } public focus(): void { - this._widget.focus(); + this._contentHoverWidget.focus(); + } + + public focusHoverPartWithIndex(index: number): void { + this._renderedContentHover?.focusHoverPartWithIndex(index); } public scrollUp(): void { - this._widget.scrollUp(); + this._contentHoverWidget.scrollUp(); } public scrollDown(): void { - this._widget.scrollDown(); + this._contentHoverWidget.scrollDown(); } public scrollLeft(): void { - this._widget.scrollLeft(); + this._contentHoverWidget.scrollLeft(); } public scrollRight(): void { - this._widget.scrollRight(); + this._contentHoverWidget.scrollRight(); } public pageUp(): void { - this._widget.pageUp(); + this._contentHoverWidget.pageUp(); } public pageDown(): void { - this._widget.pageDown(); + this._contentHoverWidget.pageDown(); } public goToTop(): void { - this._widget.goToTop(); + this._contentHoverWidget.goToTop(); } public goToBottom(): void { - this._widget.goToBottom(); + this._contentHoverWidget.goToBottom(); } public hide(): void { @@ -431,26 +364,26 @@ export class ContentHoverController extends Disposable implements IHoverWidget { } public get isColorPickerVisible(): boolean { - return this._widget.isColorPickerVisible; + return this._renderedContentHover?.isColorPickerVisible() ?? false; } public get isVisibleFromKeyboard(): boolean { - return this._widget.isVisibleFromKeyboard; + return this._contentHoverWidget.isVisibleFromKeyboard; } public get isVisible(): boolean { - return this._widget.isVisible; + return this._contentHoverWidget.isVisible; } public get isFocused(): boolean { - return this._widget.isFocused; + return this._contentHoverWidget.isFocused; } public get isResizing(): boolean { - return this._widget.isResizing; + return this._contentHoverWidget.isResizing; } public get widget() { - return this._widget; + return this._contentHoverWidget; } } diff --git a/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts new file mode 100644 index 00000000000..9308e6f295d --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts @@ -0,0 +1,436 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IEditorHoverContext, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ContentHoverComputer } from 'vs/editor/contrib/hover/browser/contentHoverComputer'; +import { EditorHoverStatusBar } from 'vs/editor/contrib/hover/browser/contentHoverStatusBar'; +import { HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { HoverResult } from 'vs/editor/contrib/hover/browser/contentHoverTypes'; +import * as dom from 'vs/base/browser/dom'; +import { HoverVerbosityAction } from 'vs/editor/common/languages'; +import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; +import { ColorHoverParticipant } from 'vs/editor/contrib/colorPicker/browser/colorHoverParticipant'; +import { localize } from 'vs/nls'; +import { InlayHintsHover } from 'vs/editor/contrib/inlayHints/browser/inlayHintsHover'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { HoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; + +export class RenderedContentHover extends Disposable { + + public closestMouseDistance: number | undefined; + public initialMousePosX: number | undefined; + public initialMousePosY: number | undefined; + + public readonly showAtPosition: Position; + public readonly showAtSecondaryPosition: Position; + public readonly shouldFocus: boolean; + public readonly source: HoverStartSource; + public readonly shouldAppearBeforeContent: boolean; + + private readonly _renderedHoverParts: RenderedContentHoverParts; + + constructor( + editor: ICodeEditor, + hoverResult: HoverResult, + participants: IEditorHoverParticipant[], + computer: ContentHoverComputer, + context: IEditorHoverContext, + keybindingService: IKeybindingService + ) { + super(); + const anchor = hoverResult.anchor; + const parts = hoverResult.hoverParts; + this._renderedHoverParts = this._register(new RenderedContentHoverParts( + editor, + participants, + parts, + keybindingService, + context + )); + const { showAtPosition, showAtSecondaryPosition } = RenderedContentHover.computeHoverPositions(editor, anchor.range, parts); + this.shouldAppearBeforeContent = parts.some(m => m.isBeforeContent); + this.showAtPosition = showAtPosition; + this.showAtSecondaryPosition = showAtSecondaryPosition; + this.initialMousePosX = anchor.initialMousePosX; + this.initialMousePosY = anchor.initialMousePosY; + this.shouldFocus = computer.shouldFocus; + this.source = computer.source; + } + + public get domNode(): DocumentFragment { + return this._renderedHoverParts.domNode; + } + + public get domNodeHasChildren(): boolean { + return this._renderedHoverParts.domNodeHasChildren; + } + + public get focusedHoverPartIndex(): number { + return this._renderedHoverParts.focusedHoverPartIndex; + } + + public focusHoverPartWithIndex(index: number): void { + this._renderedHoverParts.focusHoverPartWithIndex(index); + } + + public getAccessibleWidgetContent(): string { + return this._renderedHoverParts.getAccessibleContent(); + } + + public getAccessibleWidgetContentAtIndex(index: number): string { + return this._renderedHoverParts.getAccessibleHoverContentAtIndex(index); + } + + public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise { + this._renderedHoverParts.updateHoverVerbosityLevel(action, index, focus); + } + + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + return this._renderedHoverParts.doesHoverAtIndexSupportVerbosityAction(index, action); + } + + public isColorPickerVisible(): boolean { + return this._renderedHoverParts.isColorPickerVisible(); + } + + public static computeHoverPositions(editor: ICodeEditor, anchorRange: Range, hoverParts: IHoverPart[]): { showAtPosition: Position; showAtSecondaryPosition: Position } { + + let startColumnBoundary = 1; + if (editor.hasModel()) { + // Ensure the range is on the current view line + const viewModel = editor._getViewModel(); + const coordinatesConverter = viewModel.coordinatesConverter; + const anchorViewRange = coordinatesConverter.convertModelRangeToViewRange(anchorRange); + const anchorViewMinColumn = viewModel.getLineMinColumn(anchorViewRange.startLineNumber); + const anchorViewRangeStart = new Position(anchorViewRange.startLineNumber, anchorViewMinColumn); + startColumnBoundary = coordinatesConverter.convertViewPositionToModelPosition(anchorViewRangeStart).column; + } + + // The anchor range is always on a single line + const anchorStartLineNumber = anchorRange.startLineNumber; + let secondaryPositionColumn = anchorRange.startColumn; + let forceShowAtRange: Range | undefined; + + for (const hoverPart of hoverParts) { + const hoverPartRange = hoverPart.range; + const hoverPartRangeOnAnchorStartLine = hoverPartRange.startLineNumber === anchorStartLineNumber; + const hoverPartRangeOnAnchorEndLine = hoverPartRange.endLineNumber === anchorStartLineNumber; + const hoverPartRangeIsOnAnchorLine = hoverPartRangeOnAnchorStartLine && hoverPartRangeOnAnchorEndLine; + if (hoverPartRangeIsOnAnchorLine) { + // this message has a range that is completely sitting on the line of the anchor + const hoverPartStartColumn = hoverPartRange.startColumn; + const minSecondaryPositionColumn = Math.min(secondaryPositionColumn, hoverPartStartColumn); + secondaryPositionColumn = Math.max(minSecondaryPositionColumn, startColumnBoundary); + } + if (hoverPart.forceShowAtRange) { + forceShowAtRange = hoverPartRange; + } + } + + let showAtPosition: Position; + let showAtSecondaryPosition: Position; + if (forceShowAtRange) { + const forceShowAtPosition = forceShowAtRange.getStartPosition(); + showAtPosition = forceShowAtPosition; + showAtSecondaryPosition = forceShowAtPosition; + } else { + showAtPosition = anchorRange.getStartPosition(); + showAtSecondaryPosition = new Position(anchorStartLineNumber, secondaryPositionColumn); + } + return { + showAtPosition, + showAtSecondaryPosition, + }; + } +} + +interface IRenderedContentHoverPart { + /** + * Type of rendered part + */ + type: 'hoverPart'; + /** + * Participant of the rendered hover part + */ + participant: IEditorHoverParticipant; + /** + * The rendered hover part + */ + hoverPart: IHoverPart; + /** + * The HTML element containing the hover status bar. + */ + hoverElement: HTMLElement; +} + +interface IRenderedContentStatusBar { + /** + * Type of rendered part + */ + type: 'statusBar'; + /** + * The HTML element containing the hover status bar. + */ + hoverElement: HTMLElement; + /** + * The actions of the hover status bar. + */ + actions: HoverAction[]; +} + +type IRenderedContentHoverPartOrStatusBar = IRenderedContentHoverPart | IRenderedContentStatusBar; + +class RenderedStatusBar implements IDisposable { + + constructor(fragment: DocumentFragment, private readonly _statusBar: EditorHoverStatusBar) { + fragment.appendChild(this._statusBar.hoverElement); + } + + get hoverElement(): HTMLElement { + return this._statusBar.hoverElement; + } + + get actions(): HoverAction[] { + return this._statusBar.actions; + } + + dispose() { + this._statusBar.dispose(); + } +} + +class RenderedContentHoverParts extends Disposable { + + private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ + description: 'content-hover-highlight', + className: 'hoverHighlight' + }); + + private readonly _renderedParts: IRenderedContentHoverPartOrStatusBar[] = []; + private readonly _fragment: DocumentFragment; + private readonly _context: IEditorHoverContext; + + private _markdownHoverParticipant: MarkdownHoverParticipant | undefined; + private _colorHoverParticipant: ColorHoverParticipant | undefined; + private _focusedHoverPartIndex: number = -1; + + constructor( + editor: ICodeEditor, + participants: IEditorHoverParticipant[], + hoverParts: IHoverPart[], + keybindingService: IKeybindingService, + context: IEditorHoverContext + ) { + super(); + this._context = context; + this._fragment = document.createDocumentFragment(); + this._register(this._renderParts(participants, hoverParts, context, keybindingService)); + this._register(this._registerListenersOnRenderedParts()); + this._register(this._createEditorDecorations(editor, hoverParts)); + this._updateMarkdownAndColorParticipantInfo(participants); + } + + private _createEditorDecorations(editor: ICodeEditor, hoverParts: IHoverPart[]): IDisposable { + if (hoverParts.length === 0) { + return Disposable.None; + } + let highlightRange = hoverParts[0].range; + for (const hoverPart of hoverParts) { + const hoverPartRange = hoverPart.range; + highlightRange = Range.plusRange(highlightRange, hoverPartRange); + } + const highlightDecoration = editor.createDecorationsCollection(); + highlightDecoration.set([{ + range: highlightRange, + options: RenderedContentHoverParts._DECORATION_OPTIONS + }]); + return toDisposable(() => { + highlightDecoration.clear(); + }); + } + + private _renderParts(participants: IEditorHoverParticipant[], hoverParts: IHoverPart[], hoverContext: IEditorHoverContext, keybindingService: IKeybindingService): IDisposable { + const statusBar = new EditorHoverStatusBar(keybindingService); + const hoverRenderingContext: IEditorHoverRenderContext = { + fragment: this._fragment, + statusBar, + ...hoverContext + }; + const disposables = new DisposableStore(); + for (const participant of participants) { + const renderedHoverParts = this._renderHoverPartsForParticipant(hoverParts, participant, hoverRenderingContext); + disposables.add(renderedHoverParts); + for (const renderedHoverPart of renderedHoverParts.renderedHoverParts) { + this._renderedParts.push({ + type: 'hoverPart', + participant, + hoverPart: renderedHoverPart.hoverPart, + hoverElement: renderedHoverPart.hoverElement, + }); + } + } + const renderedStatusBar = this._renderStatusBar(this._fragment, statusBar); + if (renderedStatusBar) { + disposables.add(renderedStatusBar); + this._renderedParts.push({ + type: 'statusBar', + hoverElement: renderedStatusBar.hoverElement, + actions: renderedStatusBar.actions, + }); + } + return toDisposable(() => { disposables.dispose(); }); + } + + private _renderHoverPartsForParticipant(hoverParts: IHoverPart[], participant: IEditorHoverParticipant, hoverRenderingContext: IEditorHoverRenderContext): IRenderedHoverParts { + const hoverPartsForParticipant = hoverParts.filter(hoverPart => hoverPart.owner === participant); + const hasHoverPartsForParticipant = hoverPartsForParticipant.length > 0; + if (!hasHoverPartsForParticipant) { + return new RenderedHoverParts([]); + } + return participant.renderHoverParts(hoverRenderingContext, hoverPartsForParticipant); + } + + private _renderStatusBar(fragment: DocumentFragment, statusBar: EditorHoverStatusBar): RenderedStatusBar | undefined { + if (!statusBar.hasContent) { + return undefined; + } + return new RenderedStatusBar(fragment, statusBar); + } + + private _registerListenersOnRenderedParts(): IDisposable { + const disposables = new DisposableStore(); + this._renderedParts.forEach((renderedPart: IRenderedContentHoverPartOrStatusBar, index: number) => { + const element = renderedPart.hoverElement; + element.tabIndex = 0; + disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_IN, (event: Event) => { + event.stopPropagation(); + this._focusedHoverPartIndex = index; + })); + disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_OUT, (event: Event) => { + event.stopPropagation(); + this._focusedHoverPartIndex = -1; + })); + }); + return disposables; + } + + private _updateMarkdownAndColorParticipantInfo(participants: IEditorHoverParticipant[]) { + const markdownHoverParticipant = participants.find(p => { + return (p instanceof MarkdownHoverParticipant) && !(p instanceof InlayHintsHover); + }); + if (markdownHoverParticipant) { + this._markdownHoverParticipant = markdownHoverParticipant as MarkdownHoverParticipant; + } + this._colorHoverParticipant = participants.find(p => p instanceof ColorHoverParticipant); + } + + public focusHoverPartWithIndex(index: number): void { + if (index < 0 || index >= this._renderedParts.length) { + return; + } + this._renderedParts[index].hoverElement.focus(); + } + + public getAccessibleContent(): string { + const content: string[] = []; + for (let i = 0; i < this._renderedParts.length; i++) { + content.push(this.getAccessibleHoverContentAtIndex(i)); + } + return content.join('\n\n'); + } + + public getAccessibleHoverContentAtIndex(index: number): string { + const renderedPart = this._renderedParts[index]; + if (!renderedPart) { + return ''; + } + if (renderedPart.type === 'statusBar') { + const statusBarDescription = [localize('hoverAccessibilityStatusBar', "This is a hover status bar.")]; + for (const action of renderedPart.actions) { + const keybinding = action.actionKeybindingLabel; + if (keybinding) { + statusBarDescription.push(localize('hoverAccessibilityStatusBarActionWithKeybinding', "It has an action with label {0} and keybinding {1}.", action.actionLabel, keybinding)); + } else { + statusBarDescription.push(localize('hoverAccessibilityStatusBarActionWithoutKeybinding', "It has an action with label {0}.", action.actionLabel)); + } + } + return statusBarDescription.join('\n'); + } + return renderedPart.participant.getAccessibleContent(renderedPart.hoverPart); + } + + public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise { + if (!this._markdownHoverParticipant) { + return; + } + const normalizedMarkdownHoverIndex = this._normalizedIndexToMarkdownHoverIndexRange(this._markdownHoverParticipant, index); + if (normalizedMarkdownHoverIndex === undefined) { + return; + } + const renderedPart = await this._markdownHoverParticipant.updateMarkdownHoverVerbosityLevel(action, normalizedMarkdownHoverIndex, focus); + if (!renderedPart) { + return; + } + this._renderedParts[index] = { + type: 'hoverPart', + participant: this._markdownHoverParticipant, + hoverPart: renderedPart.hoverPart, + hoverElement: renderedPart.hoverElement, + }; + this._context.onContentsChanged(); + } + + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + if (!this._markdownHoverParticipant) { + return false; + } + const normalizedMarkdownHoverIndex = this._normalizedIndexToMarkdownHoverIndexRange(this._markdownHoverParticipant, index); + if (normalizedMarkdownHoverIndex === undefined) { + return false; + } + return this._markdownHoverParticipant.doesMarkdownHoverAtIndexSupportVerbosityAction(normalizedMarkdownHoverIndex, action); + } + + public isColorPickerVisible(): boolean { + return this._colorHoverParticipant?.isColorPickerVisible() ?? false; + } + + private _normalizedIndexToMarkdownHoverIndexRange(markdownHoverParticipant: MarkdownHoverParticipant, index: number): number | undefined { + const renderedPart = this._renderedParts[index]; + if (!renderedPart || renderedPart.type !== 'hoverPart') { + return undefined; + } + const isHoverPartMarkdownHover = renderedPart.participant === markdownHoverParticipant; + if (!isHoverPartMarkdownHover) { + return undefined; + } + const firstIndexOfMarkdownHovers = this._renderedParts.findIndex(renderedPart => + renderedPart.type === 'hoverPart' + && renderedPart.participant === markdownHoverParticipant + ); + if (firstIndexOfMarkdownHovers === -1) { + throw new BugIndicatingError(); + } + return index - firstIndexOfMarkdownHovers; + } + + public get domNode(): DocumentFragment { + return this._fragment; + } + + public get domNodeHasChildren(): boolean { + return this._fragment.hasChildNodes(); + } + + public get focusedHoverPartIndex(): number { + return this._focusedHoverPartIndex; + } +} diff --git a/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts b/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts index 04d84593d06..bb43959419b 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts @@ -13,6 +13,8 @@ const $ = dom.$; export class EditorHoverStatusBar extends Disposable implements IEditorHoverStatusBar { public readonly hoverElement: HTMLElement; + public readonly actions: HoverAction[] = []; + private readonly actionsElement: HTMLElement; private _hasContent: boolean = false; @@ -39,7 +41,9 @@ export class EditorHoverStatusBar extends Disposable implements IEditorHoverStat const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); const keybindingLabel = keybinding ? keybinding.getLabel() : null; this._hasContent = true; - return this._register(HoverAction.render(this.actionsElement, actionOptions, keybindingLabel)); + const action = this._register(HoverAction.render(this.actionsElement, actionOptions, keybindingLabel)); + this.actions.push(action); + return action; } public append(element: HTMLElement): HTMLElement { diff --git a/src/vs/editor/contrib/hover/browser/contentHoverTypes.ts b/src/vs/editor/contrib/hover/browser/contentHoverTypes.ts index 5d32dc83560..a52d758baa7 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverTypes.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverTypes.ts @@ -3,25 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { Position } from 'vs/editor/common/core/position'; -import { HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; -import { HoverAnchor, IEditorHoverColorPickerWidget, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; export class HoverResult { constructor( public readonly anchor: HoverAnchor, - public readonly messages: IHoverPart[], + public readonly hoverParts: IHoverPart[], public readonly isComplete: boolean ) { } public filter(anchor: HoverAnchor): HoverResult { - const filteredMessages = this.messages.filter((m) => m.isValidForHoverAnchor(anchor)); - if (filteredMessages.length === this.messages.length) { + const filteredHoverParts = this.hoverParts.filter((m) => m.isValidForHoverAnchor(anchor)); + if (filteredHoverParts.length === this.hoverParts.length) { return this; } - return new FilteredHoverResult(this, this.anchor, filteredMessages, this.isComplete); + return new FilteredHoverResult(this, this.anchor, filteredHoverParts, this.isComplete); } } @@ -40,21 +37,3 @@ export class FilteredHoverResult extends HoverResult { return this.original.filter(anchor); } } - -export class ContentHoverVisibleData { - - public closestMouseDistance: number | undefined = undefined; - - constructor( - public initialMousePosX: number | undefined, - public initialMousePosY: number | undefined, - public readonly colorPicker: IEditorHoverColorPickerWidget | null, - public readonly showAtPosition: Position, - public readonly showAtSecondaryPosition: Position, - public readonly preferAbove: boolean, - public readonly stoleFocus: boolean, - public readonly source: HoverStartSource, - public readonly isBeforeContent: boolean, - public readonly disposables: DisposableStore - ) { } -} diff --git a/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts index 58903400914..37cf8740e37 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts @@ -15,7 +15,8 @@ import { IAccessibilityService } from 'vs/platform/accessibility/common/accessib import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { getHoverAccessibleViewHint, HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; import { PositionAffinity } from 'vs/editor/common/model'; -import { ContentHoverVisibleData } from 'vs/editor/contrib/hover/browser/contentHoverTypes'; +import { Emitter } from 'vs/base/common/event'; +import { RenderedContentHover } from 'vs/editor/contrib/hover/browser/contentHoverRendered'; const HORIZONTAL_SCROLLING_BY = 30; const CONTAINER_HEIGHT_PADDING = 6; @@ -25,7 +26,7 @@ export class ContentHoverWidget extends ResizableContentWidget { public static ID = 'editor.contrib.resizableContentHoverWidget'; private static _lastDimensions: dom.Dimension = new dom.Dimension(0, 0); - private _visibleData: ContentHoverVisibleData | undefined; + private _renderedHover: RenderedContentHover | undefined; private _positionPreference: ContentWidgetPositionPreference | undefined; private _minimumSize: dom.Dimension; private _contentWidth: number | undefined; @@ -34,12 +35,11 @@ export class ContentHoverWidget extends ResizableContentWidget { private readonly _hoverVisibleKey: IContextKey; private readonly _hoverFocusedKey: IContextKey; - public get isColorPickerVisible(): boolean { - return Boolean(this._visibleData?.colorPicker); - } + private readonly _onDidResize = this._register(new Emitter()); + public readonly onDidResize = this._onDidResize.event; public get isVisibleFromKeyboard(): boolean { - return (this._visibleData?.source === HoverStartSource.Keyboard); + return (this._renderedHover?.source === HoverStartSource.Keyboard); } public get isVisible(): boolean { @@ -86,13 +86,13 @@ export class ContentHoverWidget extends ResizableContentWidget { this._register(focusTracker.onDidBlur(() => { this._hoverFocusedKey.set(false); })); - this._setHoverData(undefined); + this._setRenderedHover(undefined); this._editor.addContentWidget(this); } public override dispose(): void { super.dispose(); - this._visibleData?.disposables.dispose(); + this._renderedHover?.dispose(); this._editor.removeContentWidget(this); } @@ -158,11 +158,11 @@ export class ContentHoverWidget extends ResizableContentWidget { this._updateResizableNodeMaxDimensions(); this._hover.scrollbar.scanDomNode(); this._editor.layoutContentWidget(this); - this._visibleData?.colorPicker?.layout(); + this._onDidResize.fire(); } private _findAvailableSpaceVertically(): number | undefined { - const position = this._visibleData?.showAtPosition; + const position = this._renderedHover?.showAtPosition; if (!position) { return; } @@ -223,23 +223,20 @@ export class ContentHoverWidget extends ResizableContentWidget { public isMouseGettingCloser(posx: number, posy: number): boolean { - if (!this._visibleData) { + if (!this._renderedHover) { return false; } - if ( - typeof this._visibleData.initialMousePosX === 'undefined' - || typeof this._visibleData.initialMousePosY === 'undefined' - ) { - this._visibleData.initialMousePosX = posx; - this._visibleData.initialMousePosY = posy; + if (this._renderedHover.initialMousePosX === undefined || this._renderedHover.initialMousePosY === undefined) { + this._renderedHover.initialMousePosX = posx; + this._renderedHover.initialMousePosY = posy; return false; } const widgetRect = dom.getDomNodePagePosition(this.getDomNode()); - if (typeof this._visibleData.closestMouseDistance === 'undefined') { - this._visibleData.closestMouseDistance = computeDistanceFromPointToRectangle( - this._visibleData.initialMousePosX, - this._visibleData.initialMousePosY, + if (this._renderedHover.closestMouseDistance === undefined) { + this._renderedHover.closestMouseDistance = computeDistanceFromPointToRectangle( + this._renderedHover.initialMousePosX, + this._renderedHover.initialMousePosY, widgetRect.left, widgetRect.top, widgetRect.width, @@ -255,20 +252,20 @@ export class ContentHoverWidget extends ResizableContentWidget { widgetRect.width, widgetRect.height ); - if (distance > this._visibleData.closestMouseDistance + 4 /* tolerance of 4 pixels */) { + if (distance > this._renderedHover.closestMouseDistance + 4 /* tolerance of 4 pixels */) { // The mouse is getting farther away return false; } - this._visibleData.closestMouseDistance = Math.min(this._visibleData.closestMouseDistance, distance); + this._renderedHover.closestMouseDistance = Math.min(this._renderedHover.closestMouseDistance, distance); return true; } - private _setHoverData(hoverData: ContentHoverVisibleData | undefined): void { - this._visibleData?.disposables.dispose(); - this._visibleData = hoverData; - this._hoverVisibleKey.set(!!hoverData); - this._hover.containerDomNode.classList.toggle('hidden', !hoverData); + private _setRenderedHover(renderedHover: RenderedContentHover | undefined): void { + this._renderedHover?.dispose(); + this._renderedHover = renderedHover; + this._hoverVisibleKey.set(!!renderedHover); + this._hover.containerDomNode.classList.toggle('hidden', !renderedHover); } private _updateFont(): void { @@ -298,10 +295,10 @@ export class ContentHoverWidget extends ResizableContentWidget { this._setHoverWidgetMaxDimensions(width, height); } - private _render(node: DocumentFragment, hoverData: ContentHoverVisibleData) { - this._setHoverData(hoverData); + private _render(renderedHover: RenderedContentHover) { + this._setRenderedHover(renderedHover); this._updateFont(); - this._updateContent(node); + this._updateContent(renderedHover.domNode); this._updateMaxDimensions(); this.onContentsChanged(); // Simply force a synchronous render on the editor @@ -310,33 +307,33 @@ export class ContentHoverWidget extends ResizableContentWidget { } override getPosition(): IContentWidgetPosition | null { - if (!this._visibleData) { + if (!this._renderedHover) { return null; } return { - position: this._visibleData.showAtPosition, - secondaryPosition: this._visibleData.showAtSecondaryPosition, - positionAffinity: this._visibleData.isBeforeContent ? PositionAffinity.LeftOfInjectedText : undefined, + position: this._renderedHover.showAtPosition, + secondaryPosition: this._renderedHover.showAtSecondaryPosition, + positionAffinity: this._renderedHover.shouldAppearBeforeContent ? PositionAffinity.LeftOfInjectedText : undefined, preference: [this._positionPreference ?? ContentWidgetPositionPreference.ABOVE] }; } - public showAt(node: DocumentFragment, hoverData: ContentHoverVisibleData): void { + public show(renderedHover: RenderedContentHover): void { if (!this._editor || !this._editor.hasModel()) { return; } - this._render(node, hoverData); + this._render(renderedHover); const widgetHeight = dom.getTotalHeight(this._hover.containerDomNode); - const widgetPosition = hoverData.showAtPosition; + const widgetPosition = renderedHover.showAtPosition; this._positionPreference = this._findPositionPreference(widgetHeight, widgetPosition) ?? ContentWidgetPositionPreference.ABOVE; // See https://github.com/microsoft/vscode/issues/140339 // TODO: Doing a second layout of the hover after force rendering the editor this.onContentsChanged(); - if (hoverData.stoleFocus) { + if (renderedHover.shouldFocus) { this._hover.containerDomNode.focus(); } - hoverData.colorPicker?.layout(); + this._onDidResize.fire(); // The aria label overrides the label, so if we add to it, add the contents of the hover const hoverFocused = this._hover.containerDomNode.ownerDocument.activeElement === this._hover.containerDomNode; const accessibleViewHint = hoverFocused && getHoverAccessibleViewHint( @@ -350,16 +347,16 @@ export class ContentHoverWidget extends ResizableContentWidget { } public hide(): void { - if (!this._visibleData) { + if (!this._renderedHover) { return; } - const stoleFocus = this._visibleData.stoleFocus || this._hoverFocusedKey.get(); - this._setHoverData(undefined); + const hoverStoleFocus = this._renderedHover.shouldFocus || this._hoverFocusedKey.get(); + this._setRenderedHover(undefined); this._resizableNode.maxSize = new dom.Dimension(Infinity, Infinity); this._resizableNode.clearSashHoverState(); this._hoverFocusedKey.set(false); this._editor.layoutContentWidget(this); - if (stoleFocus) { + if (hoverStoleFocus) { this._editor.focus(); } } @@ -406,9 +403,9 @@ export class ContentHoverWidget extends ResizableContentWidget { this._updateMinimumWidth(); this._resizableNode.layout(height, width); - if (this._visibleData?.showAtPosition) { + if (this._renderedHover?.showAtPosition) { const widgetHeight = dom.getTotalHeight(this._hover.containerDomNode); - this._positionPreference = this._findPositionPreference(widgetHeight, this._visibleData.showAtPosition); + this._positionPreference = this._findPositionPreference(widgetHeight, this._renderedHover.showAtPosition); } this._layoutContentWidget(); } diff --git a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts index 2998c0e3e30..41a7fbdfce9 100644 --- a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts +++ b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts @@ -18,15 +18,15 @@ import { Action, IAction } from 'vs/base/common/actions'; import { ThemeIcon } from 'vs/base/common/themables'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { labelForHoverVerbosityAction } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; namespace HoverAccessibilityHelpNLS { - export const intro = localize('intro', "Focus on the hover widget to cycle through the hover parts with the Tab key."); - export const increaseVerbosity = localize('increaseVerbosity', "- The focused hover part verbosity level can be increased with the Increase Hover Verbosity command.", INCREASE_HOVER_VERBOSITY_ACTION_ID); - export const decreaseVerbosity = localize('decreaseVerbosity', "- The focused hover part verbosity level can be decreased with the Decrease Hover Verbosity command.", DECREASE_HOVER_VERBOSITY_ACTION_ID); - export const hoverContent = localize('contentHover', "The last focused hover content is the following."); + export const introHoverPart = localize('introHoverPart', 'The focused hover part content is the following:'); + export const introHoverFull = localize('introHoverFull', 'The full focused hover content is the following:'); + export const increaseVerbosity = localize('increaseVerbosity', '- The focused hover part verbosity level can be increased with the Increase Hover Verbosity command.', INCREASE_HOVER_VERBOSITY_ACTION_ID); + export const decreaseVerbosity = localize('decreaseVerbosity', '- The focused hover part verbosity level can be decreased with the Decrease Hover Verbosity command.', DECREASE_HOVER_VERBOSITY_ACTION_ID); } export class HoverAccessibleView implements IAccessibleViewImplentation { @@ -85,7 +85,6 @@ export class HoverAccessibilityHelp implements IAccessibleViewImplentation { } } - abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAccessibleViewContentProvider { abstract provideContent(): string; @@ -97,12 +96,9 @@ abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAc private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); public readonly onDidChangeContent: Event = this._onDidChangeContent.event; - protected _markdownHoverFocusedIndex: number = -1; - private _onHoverContentsChanged: IDisposable | undefined; + protected _focusedHoverPartIndex: number = -1; - constructor( - protected readonly _hoverController: HoverController, - ) { + constructor(protected readonly _hoverController: HoverController) { super(); } @@ -111,8 +107,8 @@ abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAc return; } this._hoverController.shouldKeepOpenOnEditorMouseMoveOrLeave = true; - this._markdownHoverFocusedIndex = this._hoverController.focusedMarkdownHoverIndex(); - this._onHoverContentsChanged = this._register(this._hoverController.onHoverContentsChanged(() => { + this._focusedHoverPartIndex = this._hoverController.focusedHoverPartIndex(); + this._register(this._hoverController.onHoverContentsChanged(() => { this._onDidChangeContent.fire(); })); } @@ -121,33 +117,39 @@ abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAc if (!this._hoverController) { return; } - this._markdownHoverFocusedIndex = -1; - this._hoverController.focus(); + if (this._focusedHoverPartIndex === -1) { + this._hoverController.focus(); + } else { + this._hoverController.focusHoverPartWithIndex(this._focusedHoverPartIndex); + } + this._focusedHoverPartIndex = -1; this._hoverController.shouldKeepOpenOnEditorMouseMoveOrLeave = false; - this._onHoverContentsChanged?.dispose(); - } -} - -export class HoverAccessibilityHelpProvider extends BaseHoverAccessibleViewProvider implements IAccessibleViewContentProvider { - - public readonly options: IAccessibleViewOptions = { type: AccessibleViewType.Help }; - - constructor( - hoverController: HoverController, - ) { - super(hoverController); - } - - provideContent(): string { - return this.provideContentAtIndex(this._markdownHoverFocusedIndex); + this.dispose(); } - provideContentAtIndex(index: number): string { - const content: string[] = []; - content.push(HoverAccessibilityHelpNLS.intro); - content.push(...this._descriptionsOfVerbosityActionsForIndex(index)); - content.push(...this._descriptionOfFocusedMarkdownHoverAtIndex(index)); - return content.join('\n'); + provideContentAtIndex(focusedHoverIndex: number, includeVerbosityActions: boolean): string { + if (focusedHoverIndex !== -1) { + const accessibleContent = this._hoverController.getAccessibleWidgetContentAtIndex(focusedHoverIndex); + if (accessibleContent === undefined) { + return ''; + } + const contents: string[] = []; + if (includeVerbosityActions) { + contents.push(...this._descriptionsOfVerbosityActionsForIndex(focusedHoverIndex)); + } + contents.push(HoverAccessibilityHelpNLS.introHoverPart); + contents.push(accessibleContent); + return contents.join('\n\n'); + } else { + const accessibleContent = this._hoverController.getAccessibleWidgetContent(); + if (accessibleContent === undefined) { + return ''; + } + const contents: string[] = []; + contents.push(HoverAccessibilityHelpNLS.introHoverFull); + contents.push(accessibleContent); + return contents.join('\n\n'); + } } private _descriptionsOfVerbosityActionsForIndex(index: number): string[] { @@ -164,7 +166,7 @@ export class HoverAccessibilityHelpProvider extends BaseHoverAccessibleViewProvi } private _descriptionOfVerbosityActionForIndex(action: HoverVerbosityAction, index: number): string | undefined { - const isActionSupported = this._hoverController.doesMarkdownHoverAtIndexSupportVerbosityAction(index, action); + const isActionSupported = this._hoverController.doesHoverAtIndexSupportVerbosityAction(index, action); if (!isActionSupported) { return; } @@ -175,15 +177,18 @@ export class HoverAccessibilityHelpProvider extends BaseHoverAccessibleViewProvi return HoverAccessibilityHelpNLS.decreaseVerbosity; } } +} - protected _descriptionOfFocusedMarkdownHoverAtIndex(index: number): string[] { - const content: string[] = []; - const hoverContent = this._hoverController.markdownHoverContentAtIndex(index); - if (hoverContent) { - content.push('\n' + HoverAccessibilityHelpNLS.hoverContent); - content.push('\n' + hoverContent); - } - return content; +export class HoverAccessibilityHelpProvider extends BaseHoverAccessibleViewProvider implements IAccessibleViewContentProvider { + + public readonly options: IAccessibleViewOptions = { type: AccessibleViewType.Help }; + + constructor(hoverController: HoverController) { + super(hoverController); + } + + provideContent(): string { + return this.provideContentAtIndex(this._focusedHoverPartIndex, true); } } @@ -201,8 +206,7 @@ export class HoverAccessibleViewProvider extends BaseHoverAccessibleViewProvider } public provideContent(): string { - const hoverContent = this._hoverController.markdownHoverContentAtIndex(this._markdownHoverFocusedIndex); - return hoverContent.length > 0 ? hoverContent : this._hoverController.getWidgetContent() || HoverAccessibilityHelpNLS.intro; + return this.provideContentAtIndex(this._focusedHoverPartIndex, false); } public get actions(): IAction[] { @@ -229,16 +233,16 @@ export class HoverAccessibleViewProvider extends BaseHoverAccessibleViewProvider break; } const actionLabel = labelForHoverVerbosityAction(this._keybindingService, action); - const actionEnabled = this._hoverController.doesMarkdownHoverAtIndexSupportVerbosityAction(this._markdownHoverFocusedIndex, action); + const actionEnabled = this._hoverController.doesHoverAtIndexSupportVerbosityAction(this._focusedHoverPartIndex, action); return new Action(accessibleActionId, actionLabel, ThemeIcon.asClassName(actionCodicon), actionEnabled, () => { - editor.getAction(actionId)?.run({ index: this._markdownHoverFocusedIndex, focus: false }); + editor.getAction(actionId)?.run({ index: this._focusedHoverPartIndex, focus: false }); }); } private _initializeOptions(editor: ICodeEditor, hoverController: HoverController): void { const helpProvider = this._register(new HoverAccessibilityHelpProvider(hoverController)); this.options.language = editor.getModel()?.getLanguageId(); - this.options.customHelp = () => { return helpProvider.provideContentAtIndex(this._markdownHoverFocusedIndex); }; + this.options.customHelp = () => { return helpProvider.provideContentAtIndex(this._focusedHoverPartIndex, true); }; } } diff --git a/src/vs/editor/contrib/hover/browser/hoverActions.ts b/src/vs/editor/contrib/hover/browser/hoverActions.ts index 20a5148fd74..37eea40441a 100644 --- a/src/vs/editor/contrib/hover/browser/hoverActions.ts +++ b/src/vs/editor/contrib/hover/browser/hoverActions.ts @@ -432,7 +432,12 @@ export class IncreaseHoverVerbosityLevel extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor, args?: { index: number; focus: boolean }): void { - HoverController.get(editor)?.updateMarkdownHoverVerbosityLevel(HoverVerbosityAction.Increase, args?.index, args?.focus); + const hoverController = HoverController.get(editor); + if (!hoverController) { + return; + } + const index = args?.index !== undefined ? args.index : hoverController.focusedHoverPartIndex(); + hoverController.updateHoverVerbosityLevel(HoverVerbosityAction.Increase, index, args?.focus); } } @@ -448,6 +453,11 @@ export class DecreaseHoverVerbosityLevel extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor, args?: { index: number; focus: boolean }): void { - HoverController.get(editor)?.updateMarkdownHoverVerbosityLevel(HoverVerbosityAction.Decrease, args?.index, args?.focus); + const hoverController = HoverController.get(editor); + if (!hoverController) { + return; + } + const index = args?.index !== undefined ? args.index : hoverController.focusedHoverPartIndex(); + HoverController.get(editor)?.updateHoverVerbosityLevel(HoverVerbosityAction.Decrease, index, args?.focus); } } diff --git a/src/vs/editor/contrib/hover/browser/hoverController.ts b/src/vs/editor/contrib/hover/browser/hoverController.ts index 293902db46d..b6ef34860ca 100644 --- a/src/vs/editor/contrib/hover/browser/hoverController.ts +++ b/src/vs/editor/contrib/hover/browser/hoverController.ts @@ -417,26 +417,26 @@ export class HoverController extends Disposable implements IEditorContribution { return this._contentWidget?.widget.isResizing || false; } - public focusedMarkdownHoverIndex(): number { - return this._getOrCreateContentWidget().focusedMarkdownHoverIndex(); + public focusedHoverPartIndex(): number { + return this._getOrCreateContentWidget().focusedHoverPartIndex(); } - public markdownHoverContentAtIndex(index: number): string { - return this._getOrCreateContentWidget().markdownHoverContentAtIndex(index); + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + return this._getOrCreateContentWidget().doesHoverAtIndexSupportVerbosityAction(index, action); } - public doesMarkdownHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { - return this._getOrCreateContentWidget().doesMarkdownHoverAtIndexSupportVerbosityAction(index, action); - } - - public updateMarkdownHoverVerbosityLevel(action: HoverVerbosityAction, index?: number, focus?: boolean): void { - this._getOrCreateContentWidget().updateMarkdownHoverVerbosityLevel(action, index, focus); + public updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): void { + this._getOrCreateContentWidget().updateHoverVerbosityLevel(action, index, focus); } public focus(): void { this._contentWidget?.focus(); } + public focusHoverPartWithIndex(index: number): void { + this._contentWidget?.focusHoverPartWithIndex(index); + } + public scrollUp(): void { this._contentWidget?.scrollUp(); } @@ -473,6 +473,14 @@ export class HoverController extends Disposable implements IEditorContribution { return this._contentWidget?.getWidgetContent(); } + public getAccessibleWidgetContent(): string | undefined { + return this._contentWidget?.getAccessibleWidgetContent(); + } + + public getAccessibleWidgetContentAtIndex(index: number): string | undefined { + return this._contentWidget?.getAccessibleWidgetContentAtIndex(index); + } + public get isColorPickerVisible(): boolean | undefined { return this._contentWidget?.isColorPickerVisible; } diff --git a/src/vs/editor/contrib/hover/browser/hoverTypes.ts b/src/vs/editor/contrib/hover/browser/hoverTypes.ts index 68483d2bbfa..4b23a838478 100644 --- a/src/vs/editor/contrib/hover/browser/hoverTypes.ts +++ b/src/vs/editor/contrib/hover/browser/hoverTypes.ts @@ -94,7 +94,22 @@ export interface IEditorHoverColorPickerWidget { layout(): void; } -export interface IEditorHoverRenderContext { +export interface IEditorHoverContext { + /** + * The contents rendered inside the fragment have been changed, which means that the hover should relayout. + */ + onContentsChanged(): void; + /** + * Set the minimum dimensions of the resizable hover + */ + setMinimumDimensions?(dimensions: Dimension): void; + /** + * Hide the hover. + */ + hide(): void; +} + +export interface IEditorHoverRenderContext extends IEditorHoverContext { /** * The fragment where dom elements should be attached. */ @@ -103,22 +118,38 @@ export interface IEditorHoverRenderContext { * The status bar for actions for this hover. */ readonly statusBar: IEditorHoverStatusBar; +} + +export interface IRenderedHoverPart extends IDisposable { /** - * Set if the hover will render a color picker widget. - */ - setColorPicker(widget: IEditorHoverColorPickerWidget): void; - /** - * The contents rendered inside the fragment have been changed, which means that the hover should relayout. + * The rendered hover part. */ - onContentsChanged(): void; + hoverPart: T; /** - * Set the minimum dimensions of the resizable hover + * The HTML element containing the hover part. */ - setMinimumDimensions?(dimensions: Dimension): void; + hoverElement: HTMLElement; +} + +export interface IRenderedHoverParts extends IDisposable { /** - * Hide the hover. + * Array of rendered hover parts. */ - hide(): void; + renderedHoverParts: IRenderedHoverPart[]; +} + +/** + * Default implementation of IRenderedHoverParts. + */ +export class RenderedHoverParts implements IRenderedHoverParts { + + constructor(public readonly renderedHoverParts: IRenderedHoverPart[]) { } + + dispose() { + for (const part of this.renderedHoverParts) { + part.dispose(); + } + } } export interface IEditorHoverParticipant { @@ -127,7 +158,9 @@ export interface IEditorHoverParticipant { computeSync(anchor: HoverAnchor, lineDecorations: IModelDecoration[]): T[]; computeAsync?(anchor: HoverAnchor, lineDecorations: IModelDecoration[], token: CancellationToken): AsyncIterableObject; createLoadingMessage?(anchor: HoverAnchor): T | null; - renderHoverParts(context: IEditorHoverRenderContext, hoverParts: T[]): IDisposable; + renderHoverParts(context: IEditorHoverRenderContext, hoverParts: T[]): IRenderedHoverParts; + getAccessibleContent(hoverPart: T): string; + handleResize?(): void; } export type IEditorHoverParticipantCtor = IConstructorSignature; diff --git a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts index 1adf0bbe0b8..aaeb5796aa6 100644 --- a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts @@ -7,7 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { asArray, compareBy, numberComparator } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IMarkdownString, isEmptyMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; import { DECREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID } from 'vs/editor/contrib/hover/browser/hoverActionIds'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -15,7 +15,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IModelDecoration, ITextModel } from 'vs/editor/common/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { HoverAnchor, HoverAnchorType, HoverRangeAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, HoverRangeAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -33,6 +33,7 @@ import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser import { AsyncIterableObject } from 'vs/base/common/async'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import { getHoverProviderResultsAsAsyncIterable } from 'vs/editor/contrib/hover/browser/getHover'; +import { ICommandService } from 'vs/platform/commands/common/commands'; const $ = dom.$; const increaseHoverVerbosityIcon = registerIcon('hover-increase-verbosity', Codicon.add, nls.localize('increaseHoverVerbosity', 'Icon for increaseing hover verbosity.')); @@ -90,6 +91,7 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant { this._renderedHoverParts = new MarkdownRenderedHoverParts( hoverParts, context.fragment, + this, this._editor, this._languageService, this._openerService, + this._commandService, this._keybindingService, this._hoverService, this._configurationService, @@ -191,55 +195,65 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant { + return Promise.resolve(this._renderedHoverParts?.updateMarkdownHoverPartVerbosityLevel(action, index, focus)); } } -interface RenderedHoverPart { - renderedMarkdown: HTMLElement; - disposables: DisposableStore; - hoverSource?: HoverSource; +class RenderedMarkdownHoverPart implements IRenderedHoverPart { + + constructor( + public readonly hoverPart: MarkdownHover, + public readonly hoverElement: HTMLElement, + public readonly disposables: DisposableStore, + ) { } + + get hoverAccessibleContent(): string { + return this.hoverElement.innerText.trim(); + } + + dispose(): void { + this.disposables.dispose(); + } } -class MarkdownRenderedHoverParts extends Disposable { +class MarkdownRenderedHoverParts implements IRenderedHoverParts { + + public renderedHoverParts: RenderedMarkdownHoverPart[]; - private _renderedHoverParts: RenderedHoverPart[]; - private _focusedHoverPartIndex: number = -1; private _ongoingHoverOperations: Map = new Map(); + private readonly _disposables = new DisposableStore(); + constructor( - hoverParts: MarkdownHover[], // we own! + hoverParts: MarkdownHover[], hoverPartsContainer: DocumentFragment, + private readonly _hoverParticipant: MarkdownHoverParticipant, private readonly _editor: ICodeEditor, private readonly _languageService: ILanguageService, private readonly _openerService: IOpenerService, + private readonly _commandService: ICommandService, private readonly _keybindingService: IKeybindingService, private readonly _hoverService: IHoverService, private readonly _configurationService: IConfigurationService, private readonly _onFinishedRendering: () => void, ) { - super(); - this._renderedHoverParts = this._renderHoverParts(hoverParts, hoverPartsContainer, this._onFinishedRendering); - this._register(toDisposable(() => { - this._renderedHoverParts.forEach(renderedHoverPart => { - renderedHoverPart.disposables.dispose(); + this.renderedHoverParts = this._renderHoverParts(hoverParts, hoverPartsContainer, this._onFinishedRendering); + this._disposables.add(toDisposable(() => { + this.renderedHoverParts.forEach(renderedHoverPart => { + renderedHoverPart.dispose(); + }); + this._ongoingHoverOperations.forEach(operation => { + operation.tokenSource.dispose(true); }); - })); - this._register(toDisposable(() => { - this._ongoingHoverOperations.forEach(operation => { operation.tokenSource.dispose(true); }); })); } @@ -247,75 +261,57 @@ class MarkdownRenderedHoverParts extends Disposable { hoverParts: MarkdownHover[], hoverPartsContainer: DocumentFragment, onFinishedRendering: () => void, - ): RenderedHoverPart[] { + ): RenderedMarkdownHoverPart[] { hoverParts.sort(compareBy(hover => hover.ordinal, numberComparator)); - return hoverParts.map((hoverPart, hoverIndex) => { - const renderedHoverPart = this._renderHoverPart( - hoverIndex, - hoverPart.contents, - hoverPart.source, - onFinishedRendering - ); - hoverPartsContainer.appendChild(renderedHoverPart.renderedMarkdown); + return hoverParts.map(hoverPart => { + const renderedHoverPart = this._renderHoverPart(hoverPart, onFinishedRendering); + hoverPartsContainer.appendChild(renderedHoverPart.hoverElement); return renderedHoverPart; }); } private _renderHoverPart( - hoverPartIndex: number, - hoverContents: IMarkdownString[], - hoverSource: HoverSource | undefined, + hoverPart: MarkdownHover, onFinishedRendering: () => void - ): RenderedHoverPart { + ): RenderedMarkdownHoverPart { - const { renderedMarkdown, disposables } = this._renderMarkdownContent(hoverContents, onFinishedRendering); + const renderedMarkdownPart = this._renderMarkdownHover(hoverPart, onFinishedRendering); + const renderedMarkdownElement = renderedMarkdownPart.hoverElement; + const hoverSource = hoverPart.source; + const disposables = new DisposableStore(); + disposables.add(renderedMarkdownPart); if (!hoverSource) { - return { renderedMarkdown, disposables }; + return new RenderedMarkdownHoverPart(hoverPart, renderedMarkdownElement, disposables); } const canIncreaseVerbosity = hoverSource.supportsVerbosityAction(HoverVerbosityAction.Increase); const canDecreaseVerbosity = hoverSource.supportsVerbosityAction(HoverVerbosityAction.Decrease); if (!canIncreaseVerbosity && !canDecreaseVerbosity) { - return { renderedMarkdown, disposables, hoverSource }; + return new RenderedMarkdownHoverPart(hoverPart, renderedMarkdownElement, disposables); } const actionsContainer = $('div.verbosity-actions'); - renderedMarkdown.prepend(actionsContainer); + renderedMarkdownElement.prepend(actionsContainer); disposables.add(this._renderHoverExpansionAction(actionsContainer, HoverVerbosityAction.Increase, canIncreaseVerbosity)); disposables.add(this._renderHoverExpansionAction(actionsContainer, HoverVerbosityAction.Decrease, canDecreaseVerbosity)); - - this._register(dom.addDisposableListener(renderedMarkdown, dom.EventType.FOCUS_IN, (event: Event) => { - event.stopPropagation(); - this._focusedHoverPartIndex = hoverPartIndex; - })); - this._register(dom.addDisposableListener(renderedMarkdown, dom.EventType.FOCUS_OUT, (event: Event) => { - event.stopPropagation(); - this._focusedHoverPartIndex = -1; - })); - return { renderedMarkdown, disposables, hoverSource }; + return new RenderedMarkdownHoverPart(hoverPart, renderedMarkdownElement, disposables); } - private _renderMarkdownContent( - markdownContent: IMarkdownString[], + private _renderMarkdownHover( + markdownHover: MarkdownHover, onFinishedRendering: () => void - ): RenderedHoverPart { - const renderedMarkdown = $('div.hover-row'); - renderedMarkdown.tabIndex = 0; - const renderedMarkdownContents = $('div.hover-row-contents'); - renderedMarkdown.appendChild(renderedMarkdownContents); - const disposables = new DisposableStore(); - disposables.add(renderMarkdownInContainer( + ): IRenderedHoverPart { + const renderedMarkdownHover = renderMarkdownInContainer( this._editor, - renderedMarkdownContents, - markdownContent, + markdownHover, this._languageService, this._openerService, onFinishedRendering, - )); - return { renderedMarkdown, disposables }; + ); + return renderedMarkdownHover; } private _renderHoverExpansionAction(container: HTMLElement, action: HoverVerbosityAction, actionEnabled: boolean): DisposableStore { @@ -324,59 +320,74 @@ class MarkdownRenderedHoverParts extends Disposable { const actionElement = dom.append(container, $(ThemeIcon.asCSSSelector(isActionIncrease ? increaseHoverVerbosityIcon : decreaseHoverVerbosityIcon))); actionElement.tabIndex = 0; const hoverDelegate = new WorkbenchHoverDelegate('mouse', false, { target: container, position: { hoverPosition: HoverPosition.LEFT } }, this._configurationService, this._hoverService); - store.add(this._hoverService.setupUpdatableHover(hoverDelegate, actionElement, labelForHoverVerbosityAction(this._keybindingService, action))); + store.add(this._hoverService.setupManagedHover(hoverDelegate, actionElement, labelForHoverVerbosityAction(this._keybindingService, action))); if (!actionEnabled) { actionElement.classList.add('disabled'); return store; } actionElement.classList.add('enabled'); - const actionFunction = () => this.updateMarkdownHoverPartVerbosityLevel(action); + const actionFunction = () => this._commandService.executeCommand(action === HoverVerbosityAction.Increase ? INCREASE_HOVER_VERBOSITY_ACTION_ID : DECREASE_HOVER_VERBOSITY_ACTION_ID); store.add(new ClickAction(actionElement, actionFunction)); store.add(new KeyDownAction(actionElement, actionFunction, [KeyCode.Enter, KeyCode.Space])); return store; } - public async updateMarkdownHoverPartVerbosityLevel(action: HoverVerbosityAction, index: number = -1, focus: boolean = true): Promise { + public async updateMarkdownHoverPartVerbosityLevel(action: HoverVerbosityAction, index: number, focus: boolean = true): Promise<{ hoverPart: MarkdownHover; hoverElement: HTMLElement } | undefined> { const model = this._editor.getModel(); if (!model) { - return; + return undefined; } - const indexOfInterest = index !== -1 ? index : this._focusedHoverPartIndex; - const hoverRenderedPart = this._getRenderedHoverPartAtIndex(indexOfInterest); - if (!hoverRenderedPart || !hoverRenderedPart.hoverSource?.supportsVerbosityAction(action)) { - return; + const hoverRenderedPart = this._getRenderedHoverPartAtIndex(index); + const hoverSource = hoverRenderedPart?.hoverPart.source; + if (!hoverRenderedPart || !hoverSource?.supportsVerbosityAction(action)) { + return undefined; } - const hoverSource = hoverRenderedPart.hoverSource; const newHover = await this._fetchHover(hoverSource, model, action); if (!newHover) { - return; + return undefined; } const newHoverSource = new HoverSource(newHover, hoverSource.hoverProvider, hoverSource.hoverPosition); - const newHoverRenderedPart = this._renderHoverPart( - indexOfInterest, + const initialHoverPart = hoverRenderedPart.hoverPart; + const newHoverPart = new MarkdownHover( + this._hoverParticipant, + initialHoverPart.range, newHover.contents, - newHoverSource, + initialHoverPart.isBeforeContent, + initialHoverPart.ordinal, + newHoverSource + ); + const newHoverRenderedPart = this._renderHoverPart( + newHoverPart, this._onFinishedRendering ); - this._replaceRenderedHoverPartAtIndex(indexOfInterest, newHoverRenderedPart); + this._replaceRenderedHoverPartAtIndex(index, newHoverRenderedPart, newHoverPart); if (focus) { - this._focusOnHoverPartWithIndex(indexOfInterest); + this._focusOnHoverPartWithIndex(index); } - this._onFinishedRendering(); - } - - public markdownHoverContentAtIndex(index: number): string { - const hoverRenderedPart = this._getRenderedHoverPartAtIndex(index); - return hoverRenderedPart?.renderedMarkdown.innerText ?? ''; + return { + hoverPart: newHoverPart, + hoverElement: newHoverRenderedPart.hoverElement + }; } - public focusedMarkdownHoverIndex(): number { - return this._focusedHoverPartIndex; + public getAccessibleContent(hoverPart: MarkdownHover): string | undefined { + const renderedHoverPartIndex = this.renderedHoverParts.findIndex(renderedHoverPart => renderedHoverPart.hoverPart === hoverPart); + if (renderedHoverPartIndex === -1) { + return undefined; + } + const renderedHoverPart = this._getRenderedHoverPartAtIndex(renderedHoverPartIndex); + if (!renderedHoverPart) { + return undefined; + } + const hoverElementInnerText = renderedHoverPart.hoverElement.innerText; + const accessibleContent = hoverElementInnerText.replace(/[^\S\n\r]+/gu, ' '); + return accessibleContent; } public doesMarkdownHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { const hoverRenderedPart = this._getRenderedHoverPartAtIndex(index); - if (!hoverRenderedPart || !hoverRenderedPart.hoverSource?.supportsVerbosityAction(action)) { + const hoverSource = hoverRenderedPart?.hoverPart.source; + if (!hoverRenderedPart || !hoverSource?.supportsVerbosityAction(action)) { return false; } return true; @@ -404,76 +415,94 @@ class MarkdownRenderedHoverParts extends Disposable { return hover; } - private _replaceRenderedHoverPartAtIndex(index: number, renderedHoverPart: RenderedHoverPart): void { - if (index >= this._renderHoverParts.length || index < 0) { + private _replaceRenderedHoverPartAtIndex(index: number, renderedHoverPart: RenderedMarkdownHoverPart, hoverPart: MarkdownHover): void { + if (index >= this.renderedHoverParts.length || index < 0) { return; } - const currentRenderedHoverPart = this._renderedHoverParts[index]; - const currentRenderedMarkdown = currentRenderedHoverPart.renderedMarkdown; - currentRenderedMarkdown.replaceWith(renderedHoverPart.renderedMarkdown); - currentRenderedHoverPart.disposables.dispose(); - this._renderedHoverParts[index] = renderedHoverPart; + const currentRenderedHoverPart = this.renderedHoverParts[index]; + const currentRenderedMarkdown = currentRenderedHoverPart.hoverElement; + const renderedMarkdown = renderedHoverPart.hoverElement; + const renderedChildrenElements = Array.from(renderedMarkdown.children); + currentRenderedMarkdown.replaceChildren(...renderedChildrenElements); + const newRenderedHoverPart = new RenderedMarkdownHoverPart( + hoverPart, + currentRenderedMarkdown, + renderedHoverPart.disposables + ); + currentRenderedMarkdown.focus(); + currentRenderedHoverPart.dispose(); + this.renderedHoverParts[index] = newRenderedHoverPart; } private _focusOnHoverPartWithIndex(index: number): void { - this._renderedHoverParts[index].renderedMarkdown.focus(); + this.renderedHoverParts[index].hoverElement.focus(); + } + + private _getRenderedHoverPartAtIndex(index: number): RenderedMarkdownHoverPart | undefined { + return this.renderedHoverParts[index]; } - private _getRenderedHoverPartAtIndex(index: number): RenderedHoverPart | undefined { - return this._renderedHoverParts[index]; + public dispose(): void { + this._disposables.dispose(); } } export function renderMarkdownHovers( context: IEditorHoverRenderContext, - hoverParts: MarkdownHover[], + markdownHovers: MarkdownHover[], editor: ICodeEditor, languageService: ILanguageService, openerService: IOpenerService, -): IDisposable { +): IRenderedHoverParts { // Sort hover parts to keep them stable since they might come in async, out-of-order - hoverParts.sort(compareBy(hover => hover.ordinal, numberComparator)); - - const disposables = new DisposableStore(); - for (const hoverPart of hoverParts) { - disposables.add(renderMarkdownInContainer( + markdownHovers.sort(compareBy(hover => hover.ordinal, numberComparator)); + const renderedHoverParts: IRenderedHoverPart[] = []; + for (const markdownHover of markdownHovers) { + renderedHoverParts.push(renderMarkdownInContainer( editor, - context.fragment, - hoverPart.contents, + markdownHover, languageService, openerService, context.onContentsChanged, )); } - return disposables; + return new RenderedHoverParts(renderedHoverParts); } function renderMarkdownInContainer( editor: ICodeEditor, - container: DocumentFragment | HTMLElement, - markdownStrings: IMarkdownString[], + markdownHover: MarkdownHover, languageService: ILanguageService, openerService: IOpenerService, onFinishedRendering: () => void, -): IDisposable { - const store = new DisposableStore(); - for (const contents of markdownStrings) { - if (isEmptyMarkdownString(contents)) { +): IRenderedHoverPart { + const disposables = new DisposableStore(); + const renderedMarkdown = $('div.hover-row'); + const renderedMarkdownContents = $('div.hover-row-contents'); + renderedMarkdown.appendChild(renderedMarkdownContents); + const markdownStrings = markdownHover.contents; + for (const markdownString of markdownStrings) { + if (isEmptyMarkdownString(markdownString)) { continue; } const markdownHoverElement = $('div.markdown-hover'); const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents')); - const renderer = store.add(new MarkdownRenderer({ editor }, languageService, openerService)); - store.add(renderer.onDidRenderAsync(() => { + const renderer = disposables.add(new MarkdownRenderer({ editor }, languageService, openerService)); + disposables.add(renderer.onDidRenderAsync(() => { hoverContentsElement.className = 'hover-contents code-hover-contents'; onFinishedRendering(); })); - const renderedContents = store.add(renderer.render(contents)); + const renderedContents = disposables.add(renderer.render(markdownString)); hoverContentsElement.appendChild(renderedContents.element); - container.appendChild(markdownHoverElement); + renderedMarkdownContents.appendChild(markdownHoverElement); } - return store; + const renderedHoverPart: IRenderedHoverPart = { + hoverPart: markdownHover, + hoverElement: renderedMarkdown, + dispose() { disposables.dispose(); } + }; + return renderedHoverPart; } export function labelForHoverVerbosityAction(keybindingService: IKeybindingService, action: HoverVerbosityAction): string { diff --git a/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts index 3ed0b3fab14..86dba6a80e3 100644 --- a/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts @@ -7,7 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/resources'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -20,7 +20,7 @@ import { getCodeActions, quickFixCommandId } from 'vs/editor/contrib/codeAction/ import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; import { CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/browser/gotoError'; -import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import * as nls from 'vs/nls'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IMarker, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; @@ -90,20 +90,29 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant { if (!hoverParts.length) { - return Disposable.None; + return new RenderedHoverParts([]); } const disposables = new DisposableStore(); - hoverParts.forEach(msg => context.fragment.appendChild(this.renderMarkerHover(msg, disposables))); + const renderedHoverParts: IRenderedHoverPart[] = []; + hoverParts.forEach(hoverPart => { + const renderedMarkerHover = this._renderMarkerHover(hoverPart); + context.fragment.appendChild(renderedMarkerHover.hoverElement); + renderedHoverParts.push(renderedMarkerHover); + }); const markerHoverForStatusbar = hoverParts.length === 1 ? hoverParts[0] : hoverParts.sort((a, b) => MarkerSeverity.compare(a.marker.severity, b.marker.severity))[0]; this.renderMarkerStatusbar(context, markerHoverForStatusbar, disposables); - return disposables; + return new RenderedHoverParts(renderedHoverParts); + } + + public getAccessibleContent(hoverPart: MarkerHover): string { + return hoverPart.marker.message; } - private renderMarkerHover(markerHover: MarkerHover, disposables: DisposableStore): HTMLElement { + private _renderMarkerHover(markerHover: MarkerHover): IRenderedHoverPart { + const disposables: DisposableStore = new DisposableStore(); const hoverElement = $('div.hover-row'); - hoverElement.tabIndex = 0; const markerElement = dom.append(hoverElement, $('div.marker.hover-contents')); const { source, message, code, relatedInformation } = markerHover.marker; @@ -166,7 +175,12 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant = { + hoverPart: markerHover, + hoverElement, + dispose: () => disposables.dispose() + }; + return renderedHoverPart; } private renderMarkerStatusbar(context: IEditorHoverRenderContext, markerHover: MarkerHover, disposables: DisposableStore): void { diff --git a/src/vs/editor/contrib/hover/test/browser/contentHover.test.ts b/src/vs/editor/contrib/hover/test/browser/contentHover.test.ts index b41a164ec86..65762f5905b 100644 --- a/src/vs/editor/contrib/hover/test/browser/contentHover.test.ts +++ b/src/vs/editor/contrib/hover/test/browser/contentHover.test.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController'; +import { RenderedContentHover } from 'vs/editor/contrib/hover/browser/contentHoverRendered'; import { IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { TestCodeEditorInstantiationOptions, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; @@ -18,7 +18,7 @@ suite('Content Hover', () => { test('issue #151235: Gitlens hover shows up in the wrong place', () => { const text = 'just some text'; withTestCodeEditor(text, {}, (editor) => { - const actual = ContentHoverController.computeHoverRanges( + const actual = RenderedContentHover.computeHoverPositions( editor, new Range(5, 5, 5, 5), [{ range: new Range(4, 1, 5, 6) }] @@ -27,8 +27,7 @@ suite('Content Hover', () => { actual, { showAtPosition: new Position(5, 5), - showAtSecondaryPosition: new Position(5, 5), - highlightRange: new Range(4, 1, 5, 6) + showAtSecondaryPosition: new Position(5, 5) } ); }); @@ -38,7 +37,7 @@ suite('Content Hover', () => { const text = 'just some text'; const opts: TestCodeEditorInstantiationOptions = { wordWrap: 'wordWrapColumn', wordWrapColumn: 6 }; withTestCodeEditor(text, opts, (editor) => { - const actual = ContentHoverController.computeHoverRanges( + const actual = RenderedContentHover.computeHoverPositions( editor, new Range(1, 8, 1, 8), [{ range: new Range(1, 1, 1, 15) }] @@ -47,8 +46,7 @@ suite('Content Hover', () => { actual, { showAtPosition: new Position(1, 8), - showAtSecondaryPosition: new Position(1, 6), - highlightRange: new Range(1, 1, 1, 15) + showAtSecondaryPosition: new Position(1, 6) } ); }); diff --git a/src/vs/editor/contrib/indentation/browser/indentation.ts b/src/vs/editor/contrib/indentation/browser/indentation.ts index f7d859c5d5a..84760fdb6f4 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -418,6 +418,10 @@ export class AutoIndentOnPaste implements IEditorContribution { if (!model) { return; } + const containsOnlyWhitespace = this.rangeContainsOnlyWhitespaceCharacters(model, range); + if (containsOnlyWhitespace) { + return; + } if (isStartOrEndInString(model, range)) { return; } @@ -466,7 +470,7 @@ export class AutoIndentOnPaste implements IEditorContribution { range: new Range(startLineNumber, 1, startLineNumber, oldIndentation.length + 1), text: newIndent }); - firstLineText = newIndent + firstLineText.substr(oldIndentation.length); + firstLineText = newIndent + firstLineText.substring(oldIndentation.length); } else { const indentMetadata = getIndentMetadata(model, startLineNumber, this._languageConfigurationService); @@ -546,6 +550,35 @@ export class AutoIndentOnPaste implements IEditorContribution { } } + private rangeContainsOnlyWhitespaceCharacters(model: ITextModel, range: Range): boolean { + const lineContainsOnlyWhitespace = (content: string): boolean => { + return content.trim().length === 0; + }; + let containsOnlyWhitespace: boolean = true; + if (range.startLineNumber === range.endLineNumber) { + const lineContent = model.getLineContent(range.startLineNumber); + const linePart = lineContent.substring(range.startColumn - 1, range.endColumn - 1); + containsOnlyWhitespace = lineContainsOnlyWhitespace(linePart); + } else { + for (let i = range.startLineNumber; i <= range.endLineNumber; i++) { + const lineContent = model.getLineContent(i); + if (i === range.startLineNumber) { + const linePart = lineContent.substring(range.startColumn - 1); + containsOnlyWhitespace = lineContainsOnlyWhitespace(linePart); + } else if (i === range.endLineNumber) { + const linePart = lineContent.substring(0, range.endColumn - 1); + containsOnlyWhitespace = lineContainsOnlyWhitespace(linePart); + } else { + containsOnlyWhitespace = model.getLineFirstNonWhitespaceColumn(i) === 0; + } + if (!containsOnlyWhitespace) { + break; + } + } + } + return containsOnlyWhitespace; + } + private shouldIgnoreLine(model: ITextModel, lineNumber: number): boolean { model.tokenization.forceTokenization(lineNumber); const nonWhitespaceColumn = model.getLineFirstNonWhitespaceColumn(lineNumber); diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index d6adda566d8..2beed4f823b 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; @@ -22,7 +22,10 @@ import { goIndentationRules, htmlIndentationRules, javascriptIndentationRules, l import { cppOnEnterRules, htmlOnEnterRules, javascriptOnEnterRules, phpOnEnterRules } from 'vs/editor/test/common/modes/supports/onEnterRules'; import { TypeOperations } from 'vs/editor/common/cursor/cursorTypeOperations'; import { cppBracketRules, goBracketRules, htmlBracketRules, latexBracketRules, luaBracketRules, phpBracketRules, rubyBracketRules, typescriptBracketRules, vbBracketRules } from 'vs/editor/test/common/modes/supports/bracketRules'; -import { latexAutoClosingPairsRules } from 'vs/editor/test/common/modes/supports/autoClosingPairsRules'; +import { javascriptAutoClosingPairsRules, latexAutoClosingPairsRules } from 'vs/editor/test/common/modes/supports/autoClosingPairsRules'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; export enum Language { TypeScript = 'ts-test', @@ -44,16 +47,11 @@ function testIndentationToTabsCommand(lines: string[], selection: Selection, tab testCommand(lines, null, selection, (accessor, sel) => new IndentationToTabsCommand(sel, tabSize), expectedLines, expectedSelection); } -export function registerLanguage(instantiationService: TestInstantiationService, language: Language): IDisposable { - const disposables = new DisposableStore(); - const languageService = instantiationService.get(ILanguageService); - disposables.add(registerLanguageConfiguration(instantiationService, language)); - disposables.add(languageService.registerLanguage({ id: language })); - return disposables; +export function registerLanguage(languageService: ILanguageService, language: Language): IDisposable { + return languageService.registerLanguage({ id: language }); } -export function registerLanguageConfiguration(instantiationService: TestInstantiationService, language: Language): IDisposable { - const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); +export function registerLanguageConfiguration(languageConfigurationService: ILanguageConfigurationService, language: Language): IDisposable { switch (language) { case Language.TypeScript: return languageConfigurationService.register(language, { @@ -62,6 +60,7 @@ export function registerLanguageConfiguration(instantiationService: TestInstanti lineComment: '//', blockComment: ['/*', '*/'] }, + autoClosingPairs: javascriptAutoClosingPairsRules, indentationRules: javascriptIndentationRules, onEnterRules: javascriptOnEnterRules }); @@ -317,9 +316,20 @@ suite('Indent With Tab - TypeScript/JavaScript', () => { const languageId = Language.TypeScript; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -343,9 +353,7 @@ suite('Indent With Tab - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(1, 1, 3, 5)); editor.executeCommands('editor.action.indentLines', TypeOperations.indent(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); assert.strictEqual(model.getValue(), [ @@ -369,9 +377,7 @@ suite('Indent With Tab - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(1, 1, 5, 2)); editor.executeCommands('editor.action.indentLines', TypeOperations.indent(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); assert.strictEqual(model.getValue(), [ @@ -389,9 +395,20 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const languageId = Language.TypeScript; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -405,7 +422,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { const pasteText = [ '/**', ' * JSDoc', @@ -439,7 +456,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { { startIndex: 15, standardTokenType: StandardTokenType.Other }, ] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(pasteText, true, undefined, 'keyboard'); @@ -453,7 +469,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { // no need for tokenization because there are no comments const pasteText = [ @@ -470,7 +486,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { '}' ].join('\n'); - disposables.add(registerLanguage(instantiationService, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(pasteText, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(1, 1, 11, 2)); @@ -488,8 +503,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(2, 6, 2, 6)); const text = ', null'; viewModel.paste(text, true, undefined, 'keyboard'); @@ -516,8 +530,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(5, 24, 5, 34)); const text = 'IMacLinuxKeyMapping'; viewModel.paste(text, true, undefined, 'keyboard'); @@ -541,8 +554,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel('', languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { const text = [ '/*----------------', ' * Copyright (c) ', @@ -569,7 +581,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel(initialText, languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { const tokens: StandardTokenTypeData[][] = [ [ { startIndex: 0, standardTokenType: StandardTokenType.Other }, @@ -582,7 +594,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { { startIndex: 0, standardTokenType: StandardTokenType.String }, ] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); editor.setSelection(new Selection(2, 10, 2, 15)); @@ -602,7 +613,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { const text = [ '/**', ' * @typedef {', @@ -634,7 +645,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { { startIndex: 3, standardTokenType: StandardTokenType.Other }, ] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); @@ -654,7 +664,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(2, 1, 2, 1)); const text = [ '() => {', @@ -662,7 +672,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { '}', '' ].join('\n'); - disposables.add(registerLanguage(instantiationService, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(2, 1, 5, 1)); @@ -696,7 +705,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(2, 5, 2, 5)); const text = [ '() => {', @@ -704,7 +713,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { '}', ' ' ].join('\n'); - disposables.add(registerLanguage(instantiationService, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); // todo@aiday-mar, make sure range is correct, and make test work as in real life @@ -727,7 +735,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel('', languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(2, 5, 2, 5)); const text = [ 'function makeSub(a,b) {', @@ -735,7 +743,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { 'return subsent;', '}', ].join('\n'); - disposables.add(registerLanguage(instantiationService, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); // todo@aiday-mar, make sure range is correct, and make test work as in real life @@ -760,7 +767,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { const tokens: StandardTokenTypeData[][] = [ [ { startIndex: 0, standardTokenType: StandardTokenType.Other }, @@ -791,7 +798,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { { startIndex: 0, standardTokenType: StandardTokenType.Other }, { startIndex: 1, standardTokenType: StandardTokenType.Other }] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); editor.setSelection(new Selection(2, 1, 2, 1)); @@ -799,7 +805,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { '// comment', 'const foo = 42', ].join('\n'); - disposables.add(registerLanguage(instantiationService, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(2, 1, 3, 15)); @@ -817,9 +822,20 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { const languageId = Language.TypeScript; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -837,8 +853,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { viewModel.type('const add1 = (n) =>'); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -859,8 +874,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 9, 3, 9)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -879,10 +893,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); - + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { viewModel.type([ 'const add1 = (n) =>', ' n + 1;', @@ -908,9 +919,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 1, 3, 1)); viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -939,8 +948,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'advanced' }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: 'advanced', serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(7, 6, 7, 6)); viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(), @@ -970,8 +978,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'advanced' }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: 'advanced', serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(1, 4, 1, 4)); viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(), @@ -996,8 +1003,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 12, 2, 12)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1020,9 +1026,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 19, 2, 19)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1055,9 +1059,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { const tokens: StandardTokenTypeData[][] = [ [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }], [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }], @@ -1075,6 +1077,38 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { }); }); + test('issue #209802: allman style braces in JavaScript', () => { + + // https://github.com/microsoft/vscode/issues/209802 + + const model = createTextModel([ + 'if (/*condition*/)', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(1, 19, 1, 19)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (/*condition*/)', + ' ' + ].join('\n')); + viewModel.type("{", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (/*condition*/)', + '{}' + ].join('\n')); + editor.setSelection(new Selection(2, 2, 2, 2)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (/*condition*/)', + '{', + ' ', + '}' + ].join('\n')); + }); + }); + // Failing tests... test.skip('issue #43244: indent after equal sign is detected', () => { @@ -1090,8 +1124,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(1, 14, 1, 14)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1114,8 +1147,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 7, 2, 7)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1138,8 +1170,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 7, 2, 7)); viewModel.type("\n", 'keyboard'); viewModel.type("."); @@ -1163,8 +1194,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 24, 2, 24)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1189,8 +1219,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 5, 3, 5)); viewModel.type("."); assert.strictEqual(model.getValue(), [ @@ -1213,8 +1242,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 25, 2, 25)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1233,10 +1261,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { const model = createTextModel('function foo() {}', languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); - + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(1, 17, 1, 17)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1267,9 +1292,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 14, 3, 14)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1295,9 +1318,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(4, 1, 4, 1)); viewModel.type("}", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1319,16 +1340,13 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 5, 2, 5)); viewModel.type("{}", 'keyboard'); assert.strictEqual(model.getValue(), [ 'if (true)', '{}', ].join('\n')); - editor.setSelection(new Selection(2, 2, 2, 2)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1350,8 +1368,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "keep" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "keep", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 5, 2, 5)); viewModel.type("}", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1366,9 +1383,20 @@ suite('Auto Indent On Type - Ruby', () => { const languageId = Language.Ruby; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1384,10 +1412,7 @@ suite('Auto Indent On Type - Ruby', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); - + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { viewModel.type("def foo\n i"); viewModel.type("n", 'keyboard'); assert.strictEqual(model.getValue(), "def foo\n in"); @@ -1412,10 +1437,7 @@ suite('Auto Indent On Type - Ruby', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); - + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { viewModel.type("method('#foo') do"); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1430,9 +1452,20 @@ suite('Auto Indent On Type - PHP', () => { const languageId = Language.PHP; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1448,9 +1481,7 @@ suite('Auto Indent On Type - PHP', () => { const model = createTextModel("preg_replace('{');", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { const tokens: StandardTokenTypeData[][] = [ [ { startIndex: 0, standardTokenType: StandardTokenType.Other }, @@ -1473,9 +1504,20 @@ suite('Auto Indent On Paste - Go', () => { const languageId = Language.Go; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1500,8 +1542,7 @@ suite('Auto Indent On Paste - Go', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 1, 3, 1)); const text = ' '; const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); @@ -1521,9 +1562,20 @@ suite('Auto Indent On Type - CPP', () => { const languageId = Language.CPP; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1546,8 +1598,7 @@ suite('Auto Indent On Type - CPP', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 20, 2, 20)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1569,8 +1620,7 @@ suite('Auto Indent On Type - CPP', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(1, 20, 1, 20)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1591,9 +1641,7 @@ suite('Auto Indent On Type - CPP', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "none" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "none", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 3, 2, 3)); viewModel.type("}", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1609,9 +1657,20 @@ suite('Auto Indent On Type - HTML', () => { const languageId = Language.HTML; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1635,8 +1694,7 @@ suite('Auto Indent On Type - HTML', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 48, 2, 48)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1653,9 +1711,20 @@ suite('Auto Indent On Type - Visual Basic', () => { const languageId = Language.VB; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1679,8 +1748,7 @@ suite('Auto Indent On Type - Visual Basic', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(3, 10, 3, 10)); viewModel.type("f", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1697,9 +1765,20 @@ suite('Auto Indent On Type - Latex', () => { const languageId = Language.Latex; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1722,8 +1801,7 @@ suite('Auto Indent On Type - Latex', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 9, 2, 9)); viewModel.type("{", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1738,9 +1816,20 @@ suite('Auto Indent On Type - Lua', () => { const languageId = Language.Lua; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1762,8 +1851,7 @@ suite('Auto Indent On Type - Lua', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(1, 28, 1, 28)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1773,4 +1861,3 @@ suite('Auto Indent On Type - Lua', () => { }); }); }); - diff --git a/src/vs/editor/contrib/indentation/test/browser/indentationLineProcessor.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentationLineProcessor.test.ts index 77afbe5c76d..2004b864cbe 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentationLineProcessor.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentationLineProcessor.test.ts @@ -2,24 +2,39 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IndentationContextProcessor, ProcessedIndentRulesSupport } from 'vs/editor/common/languages/supports/indentationLineProcessor'; -import { Language, registerLanguage, registerTokenizationSupport, StandardTokenTypeData } from 'vs/editor/contrib/indentation/test/browser/indentation.test'; +import { Language, registerLanguage, registerLanguageConfiguration, registerTokenizationSupport, StandardTokenTypeData } from 'vs/editor/contrib/indentation/test/browser/indentation.test'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { Range } from 'vs/editor/common/core/range'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { ILanguageService } from 'vs/editor/common/languages/language'; suite('Indentation Context Processor - TypeScript/JavaScript', () => { const languageId = Language.TypeScript; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -35,13 +50,12 @@ suite('Indentation Context Processor - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { const tokens: StandardTokenTypeData[][] = [[ { startIndex: 0, standardTokenType: StandardTokenType.Other }, { startIndex: 16, standardTokenType: StandardTokenType.String }, { startIndex: 28, standardTokenType: StandardTokenType.String } ]]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); @@ -60,7 +74,7 @@ suite('Indentation Context Processor - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { const tokens: StandardTokenTypeData[][] = [ [ { startIndex: 0, standardTokenType: StandardTokenType.Other }, @@ -72,7 +86,6 @@ suite('Indentation Context Processor - TypeScript/JavaScript', () => { { startIndex: 46, standardTokenType: StandardTokenType.Other }, { startIndex: 47, standardTokenType: StandardTokenType.String } ]]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); @@ -91,7 +104,7 @@ suite('Indentation Context Processor - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { const tokens: StandardTokenTypeData[][] = [ [ { startIndex: 0, standardTokenType: StandardTokenType.Other }, @@ -104,7 +117,6 @@ suite('Indentation Context Processor - TypeScript/JavaScript', () => { { startIndex: 44, standardTokenType: StandardTokenType.Other }, ] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); @@ -120,9 +132,20 @@ suite('Processed Indent Rules Support - TypeScript/JavaScript', () => { const languageId = Language.TypeScript; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -140,7 +163,7 @@ suite('Processed Indent Rules Support - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { const tokens: StandardTokenTypeData[][] = [ [ { startIndex: 0, standardTokenType: StandardTokenType.Other } @@ -154,7 +177,6 @@ suite('Processed Indent Rules Support - TypeScript/JavaScript', () => { { startIndex: 17, standardTokenType: StandardTokenType.Comment }, ] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; @@ -177,13 +199,12 @@ suite('Processed Indent Rules Support - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { const tokens: StandardTokenTypeData[][] = [ [{ startIndex: 0, standardTokenType: StandardTokenType.Other }], [{ startIndex: 0, standardTokenType: StandardTokenType.String }], [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; @@ -206,7 +227,7 @@ suite('Processed Indent Rules Support - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { const tokens: StandardTokenTypeData[][] = [ [ { startIndex: 0, standardTokenType: StandardTokenType.Other } @@ -220,7 +241,6 @@ suite('Processed Indent Rules Support - TypeScript/JavaScript', () => { { startIndex: 18, standardTokenType: StandardTokenType.RegEx } ] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts index d49a4bb781f..05703e108d1 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts @@ -26,6 +26,7 @@ import { asCommandLink } from 'vs/editor/contrib/inlayHints/browser/inlayHints'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { ICommandService } from 'vs/platform/commands/common/commands'; class InlayHintsHoverAnchor extends HoverForeignElementAnchor { constructor( @@ -51,8 +52,9 @@ export class InlayHintsHover extends MarkdownHoverParticipant implements IEditor @IConfigurationService configurationService: IConfigurationService, @ITextModelService private readonly _resolverService: ITextModelService, @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @ICommandService commandService: ICommandService ) { - super(editor, languageService, openerService, configurationService, languageFeaturesService, keybindingService, hoverService); + super(editor, languageService, openerService, configurationService, languageFeaturesService, keybindingService, hoverService, commandService); } suggestHoverAnchor(mouseEvent: IEditorMouseEvent): HoverAnchor | null { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts index 0c93d8465ab..450f9549fb7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts @@ -34,7 +34,7 @@ export interface IGhostTextWidgetModel { export class GhostTextWidget extends Disposable { private readonly isDisposed = observableValue(this, false); - private readonly currentTextModel = observableFromEvent(this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel()); + private readonly currentTextModel = observableFromEvent(this, this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel()); constructor( private readonly editor: ICodeEditor, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/hoverParticipant.ts b/src/vs/editor/contrib/inlineCompletions/browser/hoverParticipant.ts index 90d4ffcba0c..7bcf0ea59f1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/hoverParticipant.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/hoverParticipant.ts @@ -12,7 +12,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IModelDecoration } from 'vs/editor/common/model'; -import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; import { InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget'; import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; @@ -94,8 +94,8 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan return []; } - renderHoverParts(context: IEditorHoverRenderContext, hoverParts: InlineCompletionsHover[]): IDisposable { - const disposableStore = new DisposableStore(); + renderHoverParts(context: IEditorHoverRenderContext, hoverParts: InlineCompletionsHover[]): IRenderedHoverParts { + const disposables = new DisposableStore(); const part = hoverParts[0]; this._telemetryService.publicLog2<{}, { @@ -104,7 +104,7 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan }>('inlineCompletionHover.shown'); if (this.accessibilityService.isScreenReaderOptimized() && !this._editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { - this.renderScreenReaderText(context, part, disposableStore); + disposables.add(this.renderScreenReaderText(context, part)); } const model = part.controller.model.get()!; @@ -115,32 +115,42 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan model.inlineCompletionsCount, model.activeCommands, ); - context.fragment.appendChild(w.getDomNode()); + const widgetNode: HTMLElement = w.getDomNode(); + context.fragment.appendChild(widgetNode); model.triggerExplicitly(); - disposableStore.add(w); + disposables.add(w); + const renderedHoverPart: IRenderedHoverPart = { + hoverPart: part, + hoverElement: widgetNode, + dispose() { disposables.dispose(); } + }; + return new RenderedHoverParts([renderedHoverPart]); + } - return disposableStore; + getAccessibleContent(hoverPart: InlineCompletionsHover): string { + return nls.localize('hoverAccessibilityStatusBar', 'There are inline completions here'); } - private renderScreenReaderText(context: IEditorHoverRenderContext, part: InlineCompletionsHover, disposableStore: DisposableStore) { + private renderScreenReaderText(context: IEditorHoverRenderContext, part: InlineCompletionsHover): IDisposable { + const disposables = new DisposableStore(); const $ = dom.$; const markdownHoverElement = $('div.hover-row.markdown-hover'); const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents', { ['aria-live']: 'assertive' })); - const renderer = disposableStore.add(new MarkdownRenderer({ editor: this._editor }, this._languageService, this._openerService)); + const renderer = disposables.add(new MarkdownRenderer({ editor: this._editor }, this._languageService, this._openerService)); const render = (code: string) => { - disposableStore.add(renderer.onDidRenderAsync(() => { + disposables.add(renderer.onDidRenderAsync(() => { hoverContentsElement.className = 'hover-contents code-hover-contents'; context.onContentsChanged(); })); const inlineSuggestionAvailable = nls.localize('inlineSuggestionFollows', "Suggestion:"); - const renderedContents = disposableStore.add(renderer.render(new MarkdownString().appendText(inlineSuggestionAvailable).appendCodeblock('text', code))); + const renderedContents = disposables.add(renderer.render(new MarkdownString().appendText(inlineSuggestionAvailable).appendCodeblock('text', code))); hoverContentsElement.replaceChildren(renderedContents.element); }; - disposableStore.add(autorun(reader => { + disposables.add(autorun(reader => { /** @description update hover */ const ghostText = part.controller.model.read(reader)?.primaryGhostText.read(reader); if (ghostText) { @@ -152,5 +162,6 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan })); context.fragment.appendChild(markdownHoverElement); + return disposables; } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts index c11920c4d65..a7c25e42677 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts @@ -3,30 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createStyleSheet2 } from 'vs/base/browser/dom'; +import { createStyleSheetFromObservable } from 'vs/base/browser/domObservable'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { timeout } from 'vs/base/common/async'; import { cancelOnDispose } from 'vs/base/common/cancellation'; -import { itemEquals, itemsEquals } from 'vs/base/common/equals'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, ITransaction, autorun, autorunHandleChanges, constObservable, derived, disposableObservableValue, observableFromEvent, observableSignal, observableValue, transaction, waitForState } from 'vs/base/common/observable'; -import { ISettableObservable, observableValueOpts } from 'vs/base/common/observableInternal/base'; -import { mapObservableArrayCached } from 'vs/base/common/observableInternal/utils'; +import { IObservable, ITransaction, autorun, constObservable, derived, observableFromEvent, observableSignal, observableValue, transaction, waitForState } from 'vs/base/common/observable'; +import { ISettableObservable } from 'vs/base/common/observableInternal/base'; +import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; +import { derivedObservableWithCache, mapObservableArrayCached } from 'vs/base/common/observableInternal/utils'; import { isUndefined } from 'vs/base/common/types'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { observableCodeEditor, reactToChange, reactToChangeWithStore } from 'vs/editor/browser/observableCodeEditor'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/commandIds'; import { GhostTextWidget } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextWidget'; import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys'; import { InlineCompletionsHintsWidget, InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget'; -import { InlineCompletionsModel, VersionIdChangeReason } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; +import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; import { SuggestWidgetAdaptor } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -44,54 +44,77 @@ export class InlineCompletionsController extends Disposable { return editor.getContribution(InlineCompletionsController.ID); } - public readonly model = this._register(disposableObservableValue('inlineCompletionModel', undefined)); - private readonly _textModelVersionId = observableValue(this, -1); - private readonly _positions = observableValueOpts({ owner: this, equalsFn: itemsEquals(itemEquals()) }, [new Position(1, 1)]); + private readonly _editorObs = observableCodeEditor(this.editor); + private readonly _positions = derived(this, reader => this._editorObs.selections.read(reader)?.map(s => s.getEndPosition()) ?? [new Position(1, 1)]); + private readonly _suggestWidgetAdaptor = this._register(new SuggestWidgetAdaptor( this.editor, - () => this.model.get()?.selectedInlineCompletion.get()?.toSingleTextEdit(undefined), - (tx) => this.updateObservables(tx, VersionIdChangeReason.Other), - (item) => { - transaction(tx => { - /** @description InlineCompletionsController.handleSuggestAccepted */ - this.updateObservables(tx, VersionIdChangeReason.Other); - this.model.get()?.handleSuggestAccepted(item); - }); - } + () => { + this._editorObs.forceUpdate(); + return this.model.get()?.selectedInlineCompletion.get()?.toSingleTextEdit(undefined); + }, + (item) => this._editorObs.forceUpdate(_tx => { + /** @description InlineCompletionsController.handleSuggestAccepted */ + this.model.get()?.handleSuggestAccepted(item); + }) )); - private readonly _enabledInConfig = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).enabled); - private readonly _isScreenReaderEnabled = observableFromEvent(this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); - private readonly _editorDictationInProgress = observableFromEvent(this._contextKeyService.onDidChangeContext, () => this._contextKeyService.getContext(this.editor.getDomNode()).getValue('editorDictation.inProgress') === true); + + private readonly _suggestWidgetSelectedItem = observableFromEvent(this, cb => this._suggestWidgetAdaptor.onDidSelectedItemChange(() => { + this._editorObs.forceUpdate(_tx => cb(undefined)); + }), () => this._suggestWidgetAdaptor.selectedItem); + + + private readonly _enabledInConfig = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).enabled); + private readonly _isScreenReaderEnabled = observableFromEvent(this, this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); + private readonly _editorDictationInProgress = observableFromEvent(this, + this._contextKeyService.onDidChangeContext, + () => this._contextKeyService.getContext(this.editor.getDomNode()).getValue('editorDictation.inProgress') === true + ); private readonly _enabled = derived(this, reader => this._enabledInConfig.read(reader) && (!this._isScreenReaderEnabled.read(reader) || !this._editorDictationInProgress.read(reader))); - private readonly _fontFamily = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).fontFamily); + private readonly _debounceValue = this._debounceService.for( + this._languageFeaturesService.inlineCompletionsProvider, + 'InlineCompletionsDebounce', + { min: 50, max: 50 } + ); + + public readonly model = derivedDisposable(this, reader => { + if (this._editorObs.isReadonly.read(reader)) { return undefined; } + const textModel = this._editorObs.model.read(reader); + if (!textModel) { return undefined; } + + const model: InlineCompletionsModel = this._instantiationService.createInstance( + InlineCompletionsModel, + textModel, + this._suggestWidgetSelectedItem, + this._editorObs.versionId, + this._positions, + this._debounceValue, + observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.suggest).preview), + observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.suggest).previewMode), + observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).mode), + this._enabled, + ); + return model; + }).recomputeInitiallyAndOnChange(this._store); private readonly _ghostTexts = derived(this, (reader) => { const model = this.model.read(reader); return model?.ghostTexts.read(reader) ?? []; }); - private readonly _stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store); - private readonly _ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => { - return store.add(this._instantiationService.createInstance(GhostTextWidget, this.editor, { + private readonly _ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => + store.add(this._instantiationService.createInstance(GhostTextWidget, this.editor, { ghostText: ghostText, minReservedLineCount: constObservable(0), targetTextModel: this.model.map(v => v?.textModel), - })); - }).recomputeInitiallyAndOnChange(this._store); - - private readonly _debounceValue = this._debounceService.for( - this._languageFeaturesService.inlineCompletionsProvider, - 'InlineCompletionsDebounce', - { min: 50, max: 50 } - ); + })) + ).recomputeInitiallyAndOnChange(this._store); private readonly _playAccessibilitySignal = observableSignal(this); - private readonly _isReadonly = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly)); - private readonly _textModel = observableFromEvent(this.editor.onDidChangeModel, () => this.editor.getModel()); - private readonly _textModelIfWritable = derived(reader => this._isReadonly.read(reader) ? undefined : this._textModel.read(reader)); + private readonly _fontFamily = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).fontFamily); constructor( public readonly editor: ICodeEditor, @@ -109,69 +132,11 @@ export class InlineCompletionsController extends Disposable { this._register(new InlineCompletionContextKeys(this._contextKeyService, this.model)); - this._register(autorun(reader => { - /** @description InlineCompletionsController.update model */ - const textModel = this._textModelIfWritable.read(reader); - transaction(tx => { - /** @description InlineCompletionsController.onDidChangeModel/readonly */ - this.model.set(undefined, tx); - this.updateObservables(tx, VersionIdChangeReason.Other); - - if (textModel) { - const model = _instantiationService.createInstance( - InlineCompletionsModel, - textModel, - this._suggestWidgetAdaptor.selectedItem, - this._textModelVersionId, - this._positions, - this._debounceValue, - observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.suggest).preview), - observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.suggest).previewMode), - observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.inlineSuggest).mode), - this._enabled, - ); - this.model.set(model, tx); - } - }); - })); - - const styleElement = this._register(createStyleSheet2()); - this._register(autorun(reader => { - const fontFamily = this._fontFamily.read(reader); - styleElement.setStyle(fontFamily === '' || fontFamily === 'default' ? `` : ` -.monaco-editor .ghost-text-decoration, -.monaco-editor .ghost-text-decoration-preview, -.monaco-editor .ghost-text { - font-family: ${fontFamily}; -}`); - })); - - const getReason = (e: IModelContentChangedEvent): VersionIdChangeReason => { - if (e.isUndoing) { return VersionIdChangeReason.Undo; } - if (e.isRedoing) { return VersionIdChangeReason.Redo; } - if (this.model.get()?.isAcceptingPartially) { return VersionIdChangeReason.AcceptWord; } - return VersionIdChangeReason.Other; - }; - this._register(editor.onDidChangeModelContent((e) => transaction(tx => - /** @description InlineCompletionsController.onDidChangeModelContent */ - this.updateObservables(tx, getReason(e)) - ))); - - this._register(editor.onDidChangeCursorPosition(e => transaction(tx => { - /** @description InlineCompletionsController.onDidChangeCursorPosition */ - this.updateObservables(tx, VersionIdChangeReason.Other); - if (e.reason === CursorChangeReason.Explicit || e.source === 'api') { - this.model.get()?.stop(tx); - } - }))); - - this._register(editor.onDidType(() => transaction(tx => { - /** @description InlineCompletionsController.onDidType */ - this.updateObservables(tx, VersionIdChangeReason.Other); + this._register(reactToChange(this._editorObs.onDidType, (_value, _changes) => { if (this._enabled.get()) { - this.model.get()?.trigger(tx); + this.model.get()?.trigger(); } - }))); + })); this._register(this._commandService.onDidExecuteCommand((e) => { // These commands don't trigger onDidType. @@ -183,22 +148,28 @@ export class InlineCompletionsController extends Disposable { 'acceptSelectedSuggestion', ]); if (commands.has(e.commandId) && editor.hasTextFocus() && this._enabled.get()) { - transaction(tx => { + this._editorObs.forceUpdate(tx => { /** @description onDidExecuteCommand */ this.model.get()?.trigger(tx); }); } })); + this._register(reactToChange(this._editorObs.selections, (_value, changes) => { + if (changes.some(e => e.reason === CursorChangeReason.Explicit || e.source === 'api')) { + this.model.get()?.stop(); + } + })); + this._register(this.editor.onDidBlurEditorWidget(() => { // This is a hidden setting very useful for debugging - if (this._contextKeyService.getContextKeyValue('accessibleViewIsShown') || this._configurationService.getValue('editor.inlineSuggest.keepOnBlur') || - editor.getOption(EditorOption.inlineSuggest).keepOnBlur) { - return; - } - if (InlineSuggestionHintsContentWidget.dropDownVisible) { + if (this._contextKeyService.getContextKeyValue('accessibleViewIsShown') + || this._configurationService.getValue('editor.inlineSuggest.keepOnBlur') + || editor.getOption(EditorOption.inlineSuggest).keepOnBlur + || InlineSuggestionHintsContentWidget.dropDownVisible) { return; } + transaction(tx => { /** @description InlineCompletionsController.onDidBlurEditorWidget */ this.model.get()?.stop(tx); @@ -220,43 +191,48 @@ export class InlineCompletionsController extends Disposable { this._suggestWidgetAdaptor.stopForceRenderingAbove(); })); - const cancellationStore = this._register(new DisposableStore()); - let lastInlineCompletionId: string | undefined = undefined; - this._register(autorunHandleChanges({ - handleChange: (context, changeSummary) => { - if (context.didChange(this._playAccessibilitySignal)) { - lastInlineCompletionId = undefined; - } - return true; - }, - }, async (reader, _) => { - /** @description InlineCompletionsController.playAccessibilitySignalAndReadSuggestion */ - this._playAccessibilitySignal.read(reader); - + const currentInlineCompletionBySemanticId = derivedObservableWithCache(this, (reader, last) => { const model = this.model.read(reader); const state = model?.state.read(reader); - if (!model || !state || !state.inlineCompletion) { - lastInlineCompletionId = undefined; - return; + if (this._suggestWidgetSelectedItem.get()) { + return last; } + return state?.inlineCompletion?.semanticId; + }); + this._register(reactToChangeWithStore(derived(reader => { + this._playAccessibilitySignal.read(reader); + currentInlineCompletionBySemanticId.read(reader); + return {}; + }), async (_value, _deltas, store) => { + /** @description InlineCompletionsController.playAccessibilitySignalAndReadSuggestion */ + const model = this.model.get(); + const state = model?.state.get(); + if (!state || !model) { return; } + const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber); - if (state.inlineCompletion.semanticId !== lastInlineCompletionId) { - cancellationStore.clear(); - lastInlineCompletionId = state.inlineCompletion.semanticId; - const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber); - - await timeout(50, cancelOnDispose(cancellationStore)); - await waitForState(this._suggestWidgetAdaptor.selectedItem, isUndefined, () => false, cancelOnDispose(cancellationStore)); - - await this._accessibilitySignalService.playSignal(AccessibilitySignal.inlineSuggestion); + await timeout(50, cancelOnDispose(store)); + await waitForState(this._suggestWidgetSelectedItem, isUndefined, () => false, cancelOnDispose(store)); - if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { - this.provideScreenReaderUpdate(state.primaryGhostText.renderForScreenReader(lineText)); - } + await this._accessibilitySignalService.playSignal(AccessibilitySignal.inlineSuggestion); + if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { + this._provideScreenReaderUpdate(state.primaryGhostText.renderForScreenReader(lineText)); } })); this._register(new InlineCompletionsHintsWidget(this.editor, this.model, this._instantiationService)); + + this._register(createStyleSheetFromObservable(derived(reader => { + const fontFamily = this._fontFamily.read(reader); + if (fontFamily === '' || fontFamily === 'default') { return ''; } + return ` +.monaco-editor .ghost-text-decoration, +.monaco-editor .ghost-text-decoration-preview, +.monaco-editor .ghost-text { + font-family: ${fontFamily}; +}`; + }))); + + // TODO@hediet this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('accessibility.verbosity.inlineCompletions')) { this.editor.updateOptions({ inlineCompletionsAccessibilityVerbose: this._configurationService.getValue('accessibility.verbosity.inlineCompletions') }); @@ -269,25 +245,14 @@ export class InlineCompletionsController extends Disposable { this._playAccessibilitySignal.trigger(tx); } - private provideScreenReaderUpdate(content: string): void { + private _provideScreenReaderUpdate(content: string): void { const accessibleViewShowing = this._contextKeyService.getContextKeyValue('accessibleViewIsShown'); const accessibleViewKeybinding = this._keybindingService.lookupKeybinding('editor.action.accessibleView'); let hint: string | undefined; if (!accessibleViewShowing && accessibleViewKeybinding && this.editor.getOption(EditorOption.inlineCompletionsAccessibilityVerbose)) { hint = localize('showAccessibleViewHint', "Inspect this in the accessible view ({0})", accessibleViewKeybinding.getAriaLabel()); } - hint ? alert(content + ', ' + hint) : alert(content); - } - - /** - * Copies over the relevant state from the text model to observables. - * This solves all kind of eventing issues, as we make sure we always operate on the latest state, - * regardless of who calls into us. - */ - private updateObservables(tx: ITransaction, changeReason: VersionIdChangeReason): void { - const newModel = this.editor.getModel(); - this._textModelVersionId.set(newModel?.getVersionId() ?? -1, tx, changeReason); - this._positions.set(this.editor.getSelections()?.map(selection => selection.getPosition()) ?? [new Position(1, 1)], tx); + alert(hint ? content + ', ' + hint : content); } public shouldShowHoverAt(range: Range) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts index 9f7d3d17ba8..2226a1ea265 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts @@ -36,7 +36,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; export class InlineCompletionsHintsWidget extends Disposable { - private readonly alwaysShowToolbar = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always'); + private readonly alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always'); private sessionPosition: Position | undefined = undefined; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index bc10c23e491..92013fd5982 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Permutation } from 'vs/base/common/arrays'; +import { compareBy, Permutation } from 'vs/base/common/arrays'; import { mapFindFirst } from 'vs/base/common/arraysFind'; import { itemsEquals } from 'vs/base/common/equals'; import { BugIndicatingError, onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; @@ -23,6 +23,7 @@ import { Command, InlineCompletionContext, InlineCompletionTriggerKind, PartialA import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource'; import { computeGhostText, singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; @@ -32,17 +33,10 @@ import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetCon import { ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -export enum VersionIdChangeReason { - Undo, - Redo, - AcceptWord, - Other, -} - export class InlineCompletionsModel extends Disposable { - private readonly _source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this.textModelVersionId, this._debounceValue)); - private readonly _isActive = observableValue(this, false); - readonly _forceUpdateExplicitlySignal = observableSignal(this); + private readonly _source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this._textModelVersionId, this._debounceValue)); + private readonly _isActive = observableValue(this, false); + private readonly _forceUpdateExplicitlySignal = observableSignal(this); // We use a semantic id to keep the same inline completion selected even if the provider reorders the completions. private readonly _selectedInlineCompletionId = observableValue(this, undefined); @@ -54,7 +48,7 @@ export class InlineCompletionsModel extends Disposable { constructor( public readonly textModel: ITextModel, public readonly selectedSuggestItem: IObservable, - public readonly textModelVersionId: IObservable, + public readonly _textModelVersionId: IObservable, private readonly _positions: IObservable, private readonly _debounceValue: IFeatureDebounceInformation, private readonly _suggestPreviewEnabled: IObservable, @@ -91,6 +85,13 @@ export class InlineCompletionsModel extends Disposable { VersionIdChangeReason.AcceptWord, ]); + private _getReason(e: IModelContentChangedEvent | undefined): VersionIdChangeReason { + if (e?.isUndoing) { return VersionIdChangeReason.Undo; } + if (e?.isRedoing) { return VersionIdChangeReason.Redo; } + if (this.isAcceptingPartially) { return VersionIdChangeReason.AcceptWord; } + return VersionIdChangeReason.Other; + } + private readonly _fetchInlineCompletionsPromise = derivedHandleChanges({ owner: this, createEmptyChangeSummary: () => ({ @@ -99,7 +100,7 @@ export class InlineCompletionsModel extends Disposable { }), handleChange: (ctx, changeSummary) => { /** @description fetch inline completions */ - if (ctx.didChange(this.textModelVersionId) && this._preserveCurrentCompletionReasons.has(ctx.change)) { + if (ctx.didChange(this._textModelVersionId) && this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) { changeSummary.preserveCurrentCompletion = true; } else if (ctx.didChange(this._forceUpdateExplicitlySignal)) { changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; @@ -114,7 +115,7 @@ export class InlineCompletionsModel extends Disposable { return undefined; } - this.textModelVersionId.read(reader); // Refetch on text change + this._textModelVersionId.read(reader); // Refetch on text change const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.get(); const suggestItem = this.selectedSuggestItem.read(reader); @@ -264,26 +265,24 @@ export class InlineCompletionsModel extends Disposable { const augmentedCompletion = mapFindFirst(candidateInlineCompletions, completion => { let r = completion.toSingleTextEdit(reader); - r = singleTextRemoveCommonPrefix(r, model, Range.fromPositions(r.range.getStartPosition(), suggestCompletion.range.getEndPosition())); + r = singleTextRemoveCommonPrefix( + r, + model, + Range.fromPositions(r.range.getStartPosition(), suggestCompletion.range.getEndPosition()) + ); return singleTextEditAugments(r, suggestCompletion) ? { completion, edit: r } : undefined; }); return augmentedCompletion; } - public readonly ghostTexts = derivedOpts({ - owner: this, - equalsFn: ghostTextsOrReplacementsEqual - }, reader => { + public readonly ghostTexts = derivedOpts({ owner: this, equalsFn: ghostTextsOrReplacementsEqual }, reader => { const v = this.state.read(reader); if (!v) { return undefined; } return v.ghostTexts; }); - public readonly primaryGhostText = derivedOpts({ - owner: this, - equalsFn: ghostTextOrReplacementEquals - }, reader => { + public readonly primaryGhostText = derivedOpts({ owner: this, equalsFn: ghostTextOrReplacementEquals }, reader => { const v = this.state.read(reader); if (!v) { return undefined; } return v?.primaryGhostText; @@ -320,6 +319,11 @@ export class InlineCompletionsModel extends Disposable { } const completion = state.inlineCompletion.toInlineCompletion(undefined); + if (completion.command) { + // Make sure the completion list will not be disposed. + completion.source.addRef(); + } + editor.pushUndoStop(); if (completion.snippetInfo) { editor.executeEdits( @@ -341,18 +345,8 @@ export class InlineCompletionsModel extends Disposable { editor.setSelections(selections, 'inlineCompletionAccept'); } - if (completion.command) { - // Make sure the completion list will not be disposed. - completion.source.addRef(); - } - - // Reset before invoking the command, since the command might cause a follow up trigger. - transaction(tx => { - this._source.clear(tx); - // Potentially, isActive will get set back to true by the typing or accept inline suggest event - // if automatic inline suggestions are enabled. - this._isActive.set(false, tx); - }); + // Reset before invoking the command, as the command might cause a follow up trigger (which we don't want to reset). + this.stop(); if (completion.command) { await this._commandService @@ -458,9 +452,7 @@ export class InlineCompletionsModel extends Disposable { completion.source.inlineCompletions, completion.sourceInlineCompletion, text.length, - { - kind, - } + { kind, } ); } } finally { @@ -485,6 +477,13 @@ export class InlineCompletionsModel extends Disposable { } } +export enum VersionIdChangeReason { + Undo, + Redo, + AcceptWord, + Other, +} + export function getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryEdit: SingleTextEdit): SingleTextEdit[] { if (positions.length === 1) { // No secondary cursor positions @@ -527,7 +526,7 @@ function substringPos(text: string, pos: Position): string { } function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { - const sortPerm = Permutation.createSortPermutation(edits, (edit1, edit2) => Range.compareRangesUsingStarts(edit1.range, edit2.range)); + const sortPerm = Permutation.createSortPermutation(edits, compareBy(e => e.range, Range.compareRangesUsingStarts)); const edit = new TextEdit(sortPerm.apply(edits)); const sortedNewRanges = edit.getNewRanges(); const newRanges = sortPerm.inverse().apply(sortedNewRanges); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts index 32b77dd23fe..1eb06aa11c1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts @@ -27,7 +27,7 @@ export class InlineCompletionsSource extends Disposable { constructor( private readonly textModel: ITextModel, - private readonly versionId: IObservable, + private readonly versionId: IObservable, private readonly _debounceValue: IFeatureDebounceInformation, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService, @@ -62,7 +62,7 @@ export class InlineCompletionsSource extends Disposable { await wait(this._debounceValue.get(this.textModel), source.token); } - if (source.token.isCancellationRequested || this.textModel.getVersionId() !== request.versionId) { + if (source.token.isCancellationRequested || this._store.isDisposed || this.textModel.getVersionId() !== request.versionId) { return false; } @@ -76,7 +76,7 @@ export class InlineCompletionsSource extends Disposable { this.languageConfigurationService ); - if (source.token.isCancellationRequested || this.textModel.getVersionId() !== request.versionId) { + if (source.token.isCancellationRequested || this._store.isDisposed || this.textModel.getVersionId() !== request.versionId) { return false; } @@ -182,7 +182,7 @@ export class UpToDateInlineCompletions implements IDisposable { private readonly inlineCompletionProviderResult: InlineCompletionProviderResult, public readonly request: UpdateRequest, private readonly _textModel: ITextModel, - private readonly _versionId: IObservable, + private readonly _versionId: IObservable, ) { const ids = _textModel.deltaDecorations([], inlineCompletionProviderResult.completions.map(i => ({ range: i.range, @@ -254,7 +254,7 @@ export class InlineCompletionWithUpdatedRange { public readonly inlineCompletion: InlineCompletionItem, public readonly decorationId: string, private readonly _textModel: ITextModel, - private readonly _modelVersion: IObservable, + private readonly _modelVersion: IObservable, ) { } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts index 28052040c32..25ccf4cd125 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts @@ -18,19 +18,19 @@ import { ILanguageConfigurationService } from 'vs/editor/common/languages/langua import { ITextModel } from 'vs/editor/common/model'; import { fixBracketsInLine } from 'vs/editor/common/model/bracketPairsTextModelPart/fixBrackets'; import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; -import { getReadonlyEmptyArray } from 'vs/editor/contrib/inlineCompletions/browser/utils'; +import { getReadonlyEmptyArray } from './utils'; import { SnippetParser, Text } from 'vs/editor/contrib/snippet/browser/snippetParser'; export async function provideInlineCompletions( registry: LanguageFeatureRegistry, - position: Position, + positionOrRange: Position | Range, model: ITextModel, context: InlineCompletionContext, token: CancellationToken = CancellationToken.None, languageConfigurationService?: ILanguageConfigurationService, ): Promise { // Important: Don't use position after the await calls, as the model could have been changed in the meantime! - const defaultReplaceRange = getDefaultRange(position, model); + const defaultReplaceRange = positionOrRange instanceof Position ? getDefaultRange(positionOrRange, model) : positionOrRange; const providers = registry.all(model); const multiMap = new SetMap>(); @@ -100,8 +100,13 @@ export async function provideInlineCompletions( } try { - const completions = await provider.provideInlineCompletions(model, position, context, token); - return completions; + if (positionOrRange instanceof Position) { + const completions = await provider.provideInlineCompletions(model, positionOrRange, context, token); + return completions; + } else { + const completions = await provider.provideInlineEdits?.(model, positionOrRange, context, token); + return completions; + } } catch (e) { onUnexpectedExternalError(e); return undefined; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts index 95d135ce324..daf73173ef4 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts @@ -3,39 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; +import { compareBy, numberComparator } from 'vs/base/common/arrays'; +import { findFirstMax } from 'vs/base/common/arraysFind'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { CompletionItemInsertTextRule, CompletionItemKind, SelectedSuggestionInfo } from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; import { SnippetSession } from 'vs/editor/contrib/snippet/browser/snippetSession'; import { CompletionItem } from 'vs/editor/contrib/suggest/browser/suggest'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; -import { IObservable, ITransaction, observableValue, transaction } from 'vs/base/common/observable'; -import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; -import { ITextModel } from 'vs/editor/common/model'; -import { compareBy, numberComparator } from 'vs/base/common/arrays'; -import { findFirstMax } from 'vs/base/common/arraysFind'; -import { singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; export class SuggestWidgetAdaptor extends Disposable { private isSuggestWidgetVisible: boolean = false; private isShiftKeyPressed = false; private _isActive = false; private _currentSuggestItemInfo: SuggestItemInfo | undefined = undefined; - - private readonly _selectedItem = observableValue(this, undefined as SuggestItemInfo | undefined); - - public get selectedItem(): IObservable { - return this._selectedItem; + public get selectedItem(): SuggestItemInfo | undefined { + return this._currentSuggestItemInfo; } + private _onDidSelectedItemChange = this._register(new Emitter()); + public readonly onDidSelectedItemChange: Event = this._onDidSelectedItemChange.event; constructor( private readonly editor: ICodeEditor, private readonly suggestControllerPreselector: () => SingleTextEdit | undefined, - private readonly checkModelVersion: (tx: ITransaction) => void, private readonly onWillAccept: (item: SuggestItemInfo) => void, ) { super(); @@ -59,8 +56,6 @@ export class SuggestWidgetAdaptor extends Disposable { this._register(suggestController.registerSelector({ priority: 100, select: (model, pos, suggestItems) => { - transaction(tx => this.checkModelVersion(tx)); - const textModel = this.editor.getModel(); if (!textModel) { // Should not happen @@ -142,11 +137,7 @@ export class SuggestWidgetAdaptor extends Disposable { this._isActive = newActive; this._currentSuggestItemInfo = newInlineCompletion; - transaction(tx => { - /** @description Update state from suggest widget */ - this.checkModelVersion(tx); - this._selectedItem.set(this._isActive ? this._currentSuggestItemInfo : undefined, tx); - }); + this._onDidSelectedItemChange.fire(); } } diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts index 648f5940596..63ab19affbe 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; import { getSecondaryEdits } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts index e160c3daa01..e318702da15 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts index 354fa36b5af..a860898c64e 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts @@ -25,7 +25,7 @@ import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import * as assert from 'assert'; +import assert from 'assert'; import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; diff --git a/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts index 298fcd4452e..58e6b00260c 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts @@ -28,7 +28,7 @@ export interface IGhostTextWidgetModel { export class GhostTextWidget extends Disposable { private readonly isDisposed = observableValue(this, false); - private readonly currentTextModel = observableFromEvent(this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel()); + private readonly currentTextModel = observableFromEvent(this, this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel()); constructor( private readonly editor: ICodeEditor, diff --git a/src/vs/editor/contrib/inlineEdit/browser/hoverParticipant.ts b/src/vs/editor/contrib/inlineEdit/browser/hoverParticipant.ts index 6c1c7337f7f..cfacc77329a 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/hoverParticipant.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/hoverParticipant.ts @@ -3,17 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { constObservable } from 'vs/base/common/observable'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { IModelDecoration } from 'vs/editor/common/model'; -import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { InlineEditController } from 'vs/editor/contrib/inlineEdit/browser/inlineEditController'; import { InlineEditHintsContentWidget } from 'vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget'; +import * as nls from 'vs/nls'; export class InlineEditHover implements IHoverPart { constructor( @@ -86,8 +87,8 @@ export class InlineEditHoverParticipant implements IEditorHoverParticipant { + const disposables = new DisposableStore(); this._telemetryService.publicLog2<{}, { owner: 'hediet'; @@ -97,9 +98,17 @@ export class InlineEditHoverParticipant implements IEditorHoverParticipant = { + hoverPart: hoverParts[0], + hoverElement: widgetNode, + dispose: () => disposables.dispose() + }; + return new RenderedHoverParts([renderedHoverPart]); + } - return disposableStore; + getAccessibleContent(hoverPart: InlineEditHover): string { + return nls.localize('hoverAccessibilityInlineEdits', 'There are inline edits here.'); } } diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts index 283e72be068..9ce9a02b2e6 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts @@ -52,9 +52,9 @@ export class InlineEditController extends Disposable { private _jumpBackPosition: Position | undefined; private _isAccepting: ISettableObservable = observableValue(this, false); - private readonly _enabled = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).enabled); - private readonly _fontFamily = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).fontFamily); - private readonly _backgroundColoring = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).backgroundColoring); + private readonly _enabled = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).enabled); + private readonly _fontFamily = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).fontFamily); + private readonly _backgroundColoring = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).backgroundColoring); constructor( @@ -84,7 +84,7 @@ export class InlineEditController extends Disposable { })); //Check if the cursor is at the ghost text - const cursorPosition = observableFromEvent(editor.onDidChangeCursorPosition, () => editor.getPosition()); + const cursorPosition = observableFromEvent(this, editor.onDidChangeCursorPosition, () => editor.getPosition()); this._register(autorun(reader => { /** @description InlineEditController.cursorPositionChanged model */ if (!this._enabled.read(reader)) { diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts index ee0b537a88c..215c85fbc0e 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts @@ -27,7 +27,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export class InlineEditHintsWidget extends Disposable { - private readonly alwaysShowToolbar = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).showToolbar === 'always'); + private readonly alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).showToolbar === 'always'); private sessionPosition: Position | undefined = undefined; diff --git a/src/vs/editor/contrib/inlineEdits/browser/commands.ts b/src/vs/editor/contrib/inlineEdits/browser/commands.ts new file mode 100644 index 00000000000..c5ce0e90296 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/commands.ts @@ -0,0 +1,185 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { transaction } from 'vs/base/common/observable'; +import { asyncTransaction } from 'vs/base/common/observableInternal/base'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { inlineEditAcceptId, inlineEditVisible, showNextInlineEditActionId, showPreviousInlineEditActionId } from 'vs/editor/contrib/inlineEdits/browser/consts'; +import { InlineEditsController } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsController'; +import * as nls from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; + + +function labelAndAlias(str: nls.ILocalizedString): { label: string, alias: string } { + return { + label: str.value, + alias: str.original, + }; +} + +export class ShowNextInlineEditAction extends EditorAction { + public static ID = showNextInlineEditActionId; + constructor() { + super({ + id: ShowNextInlineEditAction.ID, + ...labelAndAlias(nls.localize2('action.inlineEdits.showNext', "Show Next Inline Edit")), + precondition: ContextKeyExpr.and(EditorContextKeys.writable, inlineEditVisible), + kbOpts: { + weight: 100, + primary: KeyMod.Alt | KeyCode.BracketRight, + }, + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + controller?.model.get()?.next(); + } +} + +export class ShowPreviousInlineEditAction extends EditorAction { + public static ID = showPreviousInlineEditActionId; + constructor() { + super({ + id: ShowPreviousInlineEditAction.ID, + ...labelAndAlias(nls.localize2('action.inlineEdits.showPrevious', "Show Previous Inline Edit")), + precondition: ContextKeyExpr.and(EditorContextKeys.writable, inlineEditVisible), + kbOpts: { + weight: 100, + primary: KeyMod.Alt | KeyCode.BracketLeft, + }, + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + controller?.model.get()?.previous(); + } +} + +export class TriggerInlineEditAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.inlineEdits.trigger', + ...labelAndAlias(nls.localize2('action.inlineEdits.trigger', "Trigger Inline Edit")), + precondition: EditorContextKeys.writable + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + await asyncTransaction(async tx => { + /** @description triggerExplicitly from command */ + await controller?.model.get()?.triggerExplicitly(tx); + }); + } +} + +export class AcceptInlineEdit extends EditorAction { + constructor() { + super({ + id: inlineEditAcceptId, + ...labelAndAlias(nls.localize2('action.inlineEdits.accept', "Accept Inline Edit")), + precondition: inlineEditVisible, + menuOpts: { + menuId: MenuId.InlineEditsActions, + title: nls.localize('inlineEditsActions', "Accept Inline Edit"), + group: 'primary', + order: 1, + icon: Codicon.check, + }, + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.Space, + weight: 20000, + kbExpr: inlineEditVisible, + } + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + if (editor instanceof EmbeddedCodeEditorWidget) { + editor = editor.getParentEditor(); + } + const controller = InlineEditsController.get(editor); + if (controller) { + controller.model.get()?.accept(controller.editor); + controller.editor.focus(); + } + } +} + +/* +TODO@hediet +export class PinInlineEdit extends EditorAction { + constructor() { + super({ + id: 'editor.action.inlineEdits.pin', + ...labelAndAlias(nls.localize2('action.inlineEdits.pin', "Pin Inline Edit")), + precondition: undefined, + kbOpts: { + primary: KeyMod.Shift | KeyCode.Space, + weight: 20000, + } + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + if (controller) { + controller.model.get()?.togglePin(); + } + } +} + +MenuRegistry.appendMenuItem(MenuId.InlineEditsActions, { + command: { + id: 'editor.action.inlineEdits.pin', + title: nls.localize('Pin', "Pin"), + icon: Codicon.pin, + }, + group: 'primary', + order: 1, + when: isPinnedContextKey.negate(), +}); + +MenuRegistry.appendMenuItem(MenuId.InlineEditsActions, { + command: { + id: 'editor.action.inlineEdits.unpin', + title: nls.localize('Unpin', "Unpin"), + icon: Codicon.pinned, + }, + group: 'primary', + order: 1, + when: isPinnedContextKey, +});*/ + +export class HideInlineEdit extends EditorAction { + public static ID = 'editor.action.inlineEdits.hide'; + + constructor() { + super({ + id: HideInlineEdit.ID, + ...labelAndAlias(nls.localize2('action.inlineEdits.hide', "Hide Inline Edit")), + precondition: inlineEditVisible, + kbOpts: { + weight: 100, + primary: KeyCode.Escape, + } + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + transaction(tx => { + controller?.model.get()?.stop(tx); + }); + } +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/consts.ts b/src/vs/editor/contrib/inlineEdits/browser/consts.ts new file mode 100644 index 00000000000..9ad19e98a76 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/consts.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export const inlineEditAcceptId = 'editor.action.inlineEdits.accept'; + +export const showPreviousInlineEditActionId = 'editor.action.inlineEdits.showPrevious'; + +export const showNextInlineEditActionId = 'editor.action.inlineEdits.showNext'; + +export const inlineEditVisible = new RawContextKey('inlineEditsVisible', false, localize('inlineEditsVisible', "Whether an inline edit is visible")); +export const isPinnedContextKey = new RawContextKey('inlineEditsIsPinned', false, localize('isPinned', "Whether an inline edit is visible")); diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution.ts new file mode 100644 index 00000000000..ae8b7182a89 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { + TriggerInlineEditAction, ShowNextInlineEditAction, ShowPreviousInlineEditAction, + AcceptInlineEdit, HideInlineEdit, +} from 'vs/editor/contrib/inlineEdits/browser/commands'; +import { InlineEditsController } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsController'; + +registerEditorContribution(InlineEditsController.ID, InlineEditsController, EditorContributionInstantiation.Eventually); + +registerEditorAction(TriggerInlineEditAction); +registerEditorAction(ShowNextInlineEditAction); +registerEditorAction(ShowPreviousInlineEditAction); +registerEditorAction(AcceptInlineEdit); +registerEditorAction(HideInlineEdit); diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts new file mode 100644 index 00000000000..5d0afd8ca0e --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { derived, derivedObservableWithCache, IReader, ISettableObservable, observableValue } from 'vs/base/common/observable'; +import { derivedDisposable, derivedWithSetter } from 'vs/base/common/observableInternal/derived'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; +import { readHotReloadableExport } from 'vs/editor/browser/widget/diffEditor/utils'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { inlineEditVisible, isPinnedContextKey } from 'vs/editor/contrib/inlineEdits/browser/consts'; +import { InlineEditsModel } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsModel'; +import { InlineEditsWidget } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsWidget'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { bindContextKey, observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; + +export class InlineEditsController extends Disposable { + static ID = 'editor.contrib.inlineEditsController'; + + public static get(editor: ICodeEditor): InlineEditsController | null { + return editor.getContribution(InlineEditsController.ID); + } + + private readonly _enabled = observableConfigValue('editor.inlineEdits.enabled', false, this._configurationService); + private readonly _editorObs = observableCodeEditor(this.editor); + private readonly _selection = derived(this, reader => this._editorObs.cursorSelection.read(reader) ?? new Selection(1, 1, 1, 1)); + + private readonly _debounceValue = this._debounceService.for( + this._languageFeaturesService.inlineCompletionsProvider, + 'InlineEditsDebounce', + { min: 50, max: 50 } + ); + + public readonly model = derivedDisposable(this, reader => { + if (!this._enabled.read(reader)) { + return undefined; + } + if (this._editorObs.isReadonly.read(reader)) { return undefined; } + const textModel = this._editorObs.model.read(reader); + if (!textModel) { return undefined; } + + const model: InlineEditsModel = this._instantiationService.createInstance( + readHotReloadableExport(InlineEditsModel, reader), + textModel, + this._editorObs.versionId, + this._selection, + this._debounceValue, + ); + + return model; + }); + + private readonly _hadInlineEdit = derivedObservableWithCache(this, (reader, lastValue) => lastValue || this.model.read(reader)?.inlineEdit.read(reader) !== undefined); + + protected readonly _widget = derivedDisposable(this, reader => { + if (!this._hadInlineEdit.read(reader)) { return undefined; } + + return this._instantiationService.createInstance( + readHotReloadableExport(InlineEditsWidget, reader), + this.editor, + this.model.map((m, reader) => m?.inlineEdit.read(reader)), + flattenSettableObservable((reader) => this.model.read(reader)?.userPrompt ?? observableValue('empty', '')), + ); + }); + + constructor( + public readonly editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ILanguageFeatureDebounceService private readonly _debounceService: ILanguageFeatureDebounceService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + super(); + + this._register(bindContextKey(inlineEditVisible, this._contextKeyService, r => !!this.model.read(r)?.inlineEdit.read(r))); + this._register(bindContextKey(isPinnedContextKey, this._contextKeyService, r => !!this.model.read(r)?.isPinned.read(r))); + + this.model.recomputeInitiallyAndOnChange(this._store); + this._widget.recomputeInitiallyAndOnChange(this._store); + } +} + +function flattenSettableObservable(fn: (reader: IReader | undefined) => ISettableObservable): ISettableObservable { + return derivedWithSetter(undefined, reader => { + const obs = fn(reader); + return obs.read(reader); + }, (value, tx) => { + fn(undefined).set(value, tx); + }); +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts new file mode 100644 index 00000000000..812818c85b1 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts @@ -0,0 +1,289 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { timeout } from 'vs/base/common/async'; +import { CancellationToken, cancelOnDispose } from 'vs/base/common/cancellation'; +import { itemsEquals, structuralEquals } from 'vs/base/common/equals'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, ISettableObservable, ITransaction, ObservablePromise, derived, derivedHandleChanges, derivedOpts, disposableObservableValue, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction } from 'vs/base/common/observable'; +import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; +import { LineRange } from 'vs/editor/common/core/lineRange'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { Command, InlineCompletionContext, InlineCompletionTriggerKind } from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { IModelService } from 'vs/editor/common/services/model'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; +import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from 'vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions'; +import { InlineEdit } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsWidget'; + +export class InlineEditsModel extends Disposable { + private static _modelId = 0; + private static _createUniqueUri(): URI { + return URI.from({ scheme: 'inline-edits', path: new Date().toString() + String(InlineEditsModel._modelId++) }); + } + + private readonly _forceUpdateExplicitlySignal = observableSignal(this); + + // We use a semantic id to keep the same inline completion selected even if the provider reorders the completions. + private readonly _selectedInlineCompletionId = observableValue(this, undefined); + + private readonly _isActive = observableValue(this, false); + + private readonly _originalModel = derivedDisposable(() => this._modelService.createModel('', null, InlineEditsModel._createUniqueUri())).keepObserved(this._store); + private readonly _modifiedModel = derivedDisposable(() => this._modelService.createModel('', null, InlineEditsModel._createUniqueUri())).keepObserved(this._store); + + private readonly _pinnedRange = new TrackedRange(this.textModel, this._textModelVersionId); + + public readonly isPinned = this._pinnedRange.range.map(range => !!range); + + public readonly userPrompt: ISettableObservable = observableValue(this, undefined); + + constructor( + public readonly textModel: ITextModel, + public readonly _textModelVersionId: IObservable, + private readonly _selection: IObservable, + protected readonly _debounceValue: IFeatureDebounceInformation, + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IDiffProviderFactoryService private readonly _diffProviderFactoryService: IDiffProviderFactoryService, + @IModelService private readonly _modelService: IModelService, + ) { + super(); + + this._register(recomputeInitiallyAndOnChange(this._fetchInlineEditsPromise)); + } + + public readonly inlineEdit = derived(this, reader => { + return this._inlineEdit.read(reader)?.promiseResult.read(reader)?.data; + }); + + public readonly _inlineEdit = derived | undefined>(this, reader => { + const edit = this.selectedInlineEdit.read(reader); + if (!edit) { return undefined; } + const range = edit.inlineCompletion.range; + if (edit.inlineCompletion.insertText.trim() === '') { + return undefined; + } + + let newLines = edit.inlineCompletion.insertText.split(/\r\n|\r|\n/); + + function removeIndentation(lines: string[]): string[] { + const indentation = lines[0].match(/^\s*/)?.[0] ?? ''; + return lines.map(l => l.replace(new RegExp('^' + indentation), '')); + } + newLines = removeIndentation(newLines); + + const existing = this.textModel.getValueInRange(range); + let existingLines = existing.split(/\r\n|\r|\n/); + existingLines = removeIndentation(existingLines); + this._originalModel.get().setValue(existingLines.join('\n')); + this._modifiedModel.get().setValue(newLines.join('\n')); + + const d = this._diffProviderFactoryService.createDiffProvider({ diffAlgorithm: 'advanced' }); + return ObservablePromise.fromFn(async () => { + const result = await d.computeDiff(this._originalModel.get(), this._modifiedModel.get(), { + computeMoves: false, + ignoreTrimWhitespace: false, + maxComputationTimeMs: 1000, + }, CancellationToken.None); + + if (result.identical) { + return undefined; + } + + return new InlineEdit(LineRange.fromRangeInclusive(range), removeIndentation(newLines), result.changes); + }); + }); + + private readonly _fetchStore = this._register(new DisposableStore()); + + private readonly _inlineEditsFetchResult = disposableObservableValue(this, undefined); + private readonly _inlineEdits = derivedOpts({ owner: this, equalsFn: structuralEquals }, reader => { + return this._inlineEditsFetchResult.read(reader)?.completions.map(c => new InlineEditData(c)) ?? []; + }); + + private readonly _fetchInlineEditsPromise = derivedHandleChanges({ + owner: this, + createEmptyChangeSummary: () => ({ + inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic + }), + handleChange: (ctx, changeSummary) => { + /** @description fetch inline completions */ + if (ctx.didChange(this._forceUpdateExplicitlySignal)) { + changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; + } + return true; + }, + }, async (reader, changeSummary) => { + this._fetchStore.clear(); + this._forceUpdateExplicitlySignal.read(reader); + /*if (!this._isActive.read(reader)) { + return undefined; + }*/ + this._textModelVersionId.read(reader); + + function mapValue(value: T, fn: (value: T) => TOut): TOut { + return fn(value); + } + + const selection = this._pinnedRange.range.read(reader) ?? mapValue(this._selection.read(reader), v => v.isEmpty() ? undefined : v); + if (!selection) { + this._inlineEditsFetchResult.set(undefined, undefined); + this.userPrompt.set(undefined, undefined); + return undefined; + } + const context: InlineCompletionContext = { + triggerKind: changeSummary.inlineCompletionTriggerKind, + selectedSuggestionInfo: undefined, + userPrompt: this.userPrompt.read(reader), + }; + + const token = cancelOnDispose(this._fetchStore); + await timeout(200, token); + const result = await provideInlineCompletions(this.languageFeaturesService.inlineCompletionsProvider, selection, this.textModel, context, token); + if (token.isCancellationRequested) { + return; + } + + this._inlineEditsFetchResult.set(result, undefined); + }); + + public async trigger(tx?: ITransaction): Promise { + this._isActive.set(true, tx); + await this._fetchInlineEditsPromise.get(); + } + + public async triggerExplicitly(tx?: ITransaction): Promise { + subtransaction(tx, tx => { + this._isActive.set(true, tx); + this._forceUpdateExplicitlySignal.trigger(tx); + }); + await this._fetchInlineEditsPromise.get(); + } + + public stop(tx?: ITransaction): void { + subtransaction(tx, tx => { + this.userPrompt.set(undefined, tx); + this._isActive.set(false, tx); + this._inlineEditsFetchResult.set(undefined, tx); + this._pinnedRange.setRange(undefined, tx); + //this._source.clear(tx); + }); + } + + private readonly _filteredInlineEditItems = derivedOpts({ owner: this, equalsFn: itemsEquals() }, reader => { + return this._inlineEdits.read(reader); + }); + + public readonly selectedInlineCompletionIndex = derived(this, (reader) => { + const selectedInlineCompletionId = this._selectedInlineCompletionId.read(reader); + const filteredCompletions = this._filteredInlineEditItems.read(reader); + const idx = this._selectedInlineCompletionId === undefined ? -1 + : filteredCompletions.findIndex(v => v.semanticId === selectedInlineCompletionId); + if (idx === -1) { + // Reset the selection so that the selection does not jump back when it appears again + this._selectedInlineCompletionId.set(undefined, undefined); + return 0; + } + return idx; + }); + + public readonly selectedInlineEdit = derived(this, (reader) => { + const filteredCompletions = this._filteredInlineEditItems.read(reader); + const idx = this.selectedInlineCompletionIndex.read(reader); + return filteredCompletions[idx]; + }); + + public readonly activeCommands = derivedOpts({ owner: this, equalsFn: itemsEquals() }, + r => this.selectedInlineEdit.read(r)?.inlineCompletion.source.inlineCompletions.commands ?? [] + ); + + private async _deltaSelectedInlineCompletionIndex(delta: 1 | -1): Promise { + await this.triggerExplicitly(); + + const completions = this._filteredInlineEditItems.get() || []; + if (completions.length > 0) { + const newIdx = (this.selectedInlineCompletionIndex.get() + delta + completions.length) % completions.length; + this._selectedInlineCompletionId.set(completions[newIdx].semanticId, undefined); + } else { + this._selectedInlineCompletionId.set(undefined, undefined); + } + } + + public async next(): Promise { + await this._deltaSelectedInlineCompletionIndex(1); + } + + public async previous(): Promise { + await this._deltaSelectedInlineCompletionIndex(-1); + } + + public togglePin(): void { + if (this.isPinned.get()) { + this._pinnedRange.setRange(undefined, undefined); + } else { + this._pinnedRange.setRange(this._selection.get(), undefined); + } + } + + public async accept(editor: ICodeEditor): Promise { + if (editor.getModel() !== this.textModel) { + throw new BugIndicatingError(); + } + const edit = this.selectedInlineEdit.get(); + if (!edit) { + return; + } + + editor.pushUndoStop(); + editor.executeEdits( + 'inlineSuggestion.accept', + [ + edit.inlineCompletion.toSingleTextEdit().toSingleEditOperation() + ] + ); + this.stop(); + } +} + +class InlineEditData { + public readonly semanticId = this.inlineCompletion.hash(); + + constructor(public readonly inlineCompletion: InlineCompletionItem) { + + } +} + +class TrackedRange extends Disposable { + private readonly _decorations = observableValue(this, []); + + constructor( + private readonly _textModel: ITextModel, + private readonly _versionId: IObservable, + ) { + super(); + this._register(toDisposable(() => { + this._textModel.deltaDecorations(this._decorations.get(), []); + })); + } + + setRange(range: Range | undefined, tx: ITransaction | undefined): void { + this._decorations.set(this._textModel.deltaDecorations(this._decorations.get(), range ? [{ range, options: { description: 'trackedRange' } }] : []), tx); + } + + public readonly range = derived(this, reader => { + this._versionId.read(reader); + const deco = this._decorations.read(reader)[0]; + if (!deco) { return null; } + + return this._textModel.getDecorationRange(deco) ?? null; + }); +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.css b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.css new file mode 100644 index 00000000000..68910c883a6 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.css @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor div.inline-edits-widget { + --widget-color: var(--vscode-notifications-background); + + .promptEditor .monaco-editor { + --vscode-editor-placeholder-foreground: var(--vscode-editorGhostText-foreground); + } + + .toolbar, .promptEditor { + opacity: 0; + transition: opacity 0.2s ease-in-out; + } + &:hover, &.focused { + .toolbar, .promptEditor { + opacity: 1; + } + } + + .preview .monaco-editor { + + .mtk1 { + /*color: rgba(215, 215, 215, 0.452);*/ + color: var(--vscode-editorGhostText-foreground); + } + .view-overlays .current-line-exact { + border: none; + } + + .current-line-margin { + border: none; + } + + --vscode-editor-background: var(--widget-color); + } + + svg { + .gradient-start { + stop-color: var(--vscode-editor-background); + } + + .gradient-stop { + stop-color: var(--widget-color); + } + } +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts new file mode 100644 index 00000000000..0a1498915d4 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts @@ -0,0 +1,400 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { h, svgElem } from 'vs/base/browser/dom'; +import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { autorun, constObservable, derived, IObservable, ISettableObservable } from 'vs/base/common/observable'; +import { derivedWithSetter } from 'vs/base/common/observableInternal/derived'; +import 'vs/css!./inlineEditsWidget'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; +import { diffAddDecoration, diffAddDecorationEmpty, diffDeleteDecoration, diffDeleteDecorationEmpty, diffLineAddDecorationBackgroundWithIndicator, diffLineDeleteDecorationBackgroundWithIndicator, diffWholeLineAddDecoration, diffWholeLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditor/registrations.contribution'; +import { appendRemoveOnDispose, applyStyle } from 'vs/editor/browser/widget/diffEditor/utils'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { LineRange } from 'vs/editor/common/core/lineRange'; +import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; +import { PlaceholderTextContribution } from 'vs/editor/contrib/placeholderText/browser/placeholderText.contribution'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +export class InlineEdit { + constructor( + public readonly range: LineRange, + public readonly newLines: string[], + public readonly changes: readonly DetailedLineRangeMapping[], + ) { + + } +} + +export class InlineEditsWidget extends Disposable { + private readonly _editorObs = observableCodeEditor(this._editor); + + private readonly _elements = h('div.inline-edits-widget', { + style: { + position: 'absolute', + overflow: 'visible', + top: '0px', + left: '0px', + }, + }, [ + h('div@editorContainer', { style: { position: 'absolute', top: '0px', left: '0px', width: '500px', height: '500px', } }, [ + h('div.toolbar@toolbar', { style: { position: 'absolute', top: '-25px', left: '0px' } }), + h('div.promptEditor@promptEditor', { style: { position: 'absolute', top: '-25px', left: '80px', width: '300px', height: '22px' } }), + h('div.preview@editor', { style: { position: 'absolute', top: '0px', left: '0px' } }), + ]), + svgElem('svg', { style: { overflow: 'visible', pointerEvents: 'none' }, }, [ + svgElem('defs', [ + svgElem('linearGradient', { + id: 'Gradient2', + x1: '0', + y1: '0', + x2: '1', + y2: '0', + }, [ + /*svgElem('stop', { offset: '0%', class: 'gradient-start', }), + svgElem('stop', { offset: '0%', class: 'gradient-start', }), + svgElem('stop', { offset: '20%', class: 'gradient-stop', }),*/ + svgElem('stop', { offset: '0%', class: 'gradient-stop', }), + svgElem('stop', { offset: '100%', class: 'gradient-stop', }), + ]), + ]), + svgElem('path@path', { + d: '', + fill: 'url(#Gradient2)', + }), + ]), + ]); + + protected readonly _toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar, MenuId.InlineEditsActions, { + toolbarOptions: { + primaryGroup: g => g.startsWith('primary'), + }, + })); + private readonly _previewTextModel = this._register(this._instantiationService.createInstance( + TextModel, + '', + PLAINTEXT_LANGUAGE_ID, + TextModel.DEFAULT_CREATION_OPTIONS, + null + )); + + private readonly _setText = derived(reader => { + const edit = this._edit.read(reader); + if (!edit) { return; } + this._previewTextModel.setValue(edit.newLines.join('\n')); + }).recomputeInitiallyAndOnChange(this._store); + + + private readonly _promptTextModel = this._register(this._instantiationService.createInstance( + TextModel, + '', + PLAINTEXT_LANGUAGE_ID, + TextModel.DEFAULT_CREATION_OPTIONS, + null + )); + private readonly _promptEditor = this._register(this._instantiationService.createInstance( + EmbeddedCodeEditorWidget, + this._elements.promptEditor, + { + glyphMargin: false, + lineNumbers: 'off', + minimap: { enabled: false }, + guides: { + indentation: false, + bracketPairs: false, + bracketPairsHorizontal: false, + highlightActiveIndentation: false, + }, + folding: false, + selectOnLineNumbers: false, + selectionHighlight: false, + columnSelection: false, + overviewRulerBorder: false, + overviewRulerLanes: 0, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + placeholder: 'Describe the change you want...', + fontFamily: DEFAULT_FONT_FAMILY, + }, + { + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + SuggestController.ID, + PlaceholderTextContribution.ID, + ContextMenuController.ID, + ]), + isSimpleWidget: true + }, + this._editor + )); + + private readonly _previewEditor = this._register(this._instantiationService.createInstance( + EmbeddedCodeEditorWidget, + this._elements.editor, + { + glyphMargin: false, + lineNumbers: 'off', + minimap: { enabled: false }, + guides: { + indentation: false, + bracketPairs: false, + bracketPairsHorizontal: false, + highlightActiveIndentation: false, + }, + folding: false, + selectOnLineNumbers: false, + selectionHighlight: false, + columnSelection: false, + overviewRulerBorder: false, + overviewRulerLanes: 0, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + }, + { contributions: [], }, + this._editor + )); + + private readonly _previewEditorObs = observableCodeEditor(this._previewEditor); + + private readonly _decorations = derived(this, (reader) => { + this._setText.read(reader); + const diff = this._edit.read(reader)?.changes; + if (!diff) { return []; } + + const originalDecorations: IModelDeltaDecoration[] = []; + const modifiedDecorations: IModelDeltaDecoration[] = []; + + if (diff.length === 1 && diff[0].innerChanges![0].modifiedRange.equalsRange(this._previewTextModel.getFullModelRange())) { + return []; + } + + for (const m of diff) { + if (!m.original.isEmpty) { + originalDecorations.push({ range: m.original.toInclusiveRange()!, options: diffLineDeleteDecorationBackgroundWithIndicator }); + } + if (!m.modified.isEmpty) { + modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffLineAddDecorationBackgroundWithIndicator }); + } + + if (m.modified.isEmpty || m.original.isEmpty) { + if (!m.original.isEmpty) { + originalDecorations.push({ range: m.original.toInclusiveRange()!, options: diffWholeLineDeleteDecoration }); + } + if (!m.modified.isEmpty) { + modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffWholeLineAddDecoration }); + } + } else { + for (const i of m.innerChanges || []) { + // Don't show empty markers outside the line range + if (m.original.contains(i.originalRange.startLineNumber)) { + originalDecorations.push({ range: i.originalRange, options: i.originalRange.isEmpty() ? diffDeleteDecorationEmpty : diffDeleteDecoration }); + } + if (m.modified.contains(i.modifiedRange.startLineNumber)) { + modifiedDecorations.push({ range: i.modifiedRange, options: i.modifiedRange.isEmpty() ? diffAddDecorationEmpty : diffAddDecoration }); + } + } + } + } + + return modifiedDecorations; + }); + + private readonly _layout1 = derived(this, reader => { + const model = this._editor.getModel()!; + const inlineEdit = this._edit.read(reader); + if (!inlineEdit) { return null; } + + const range = inlineEdit.range; + + let maxLeft = 0; + for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) { + const column = model.getLineMaxColumn(i); + const left = this._editor.getOffsetForColumn(i, column); + maxLeft = Math.max(maxLeft, left); + } + + const layoutInfo = this._editor.getLayoutInfo(); + const contentLeft = layoutInfo.contentLeft; + + return { left: contentLeft + maxLeft }; + }); + + private readonly _layout = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + if (!inlineEdit) { return null; } + + const range = inlineEdit.range; + + const scrollLeft = this._editorObs.scrollLeft.read(reader); + + const left = this._layout1.read(reader)!.left + 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); + + const topCode = new Point(left, selectionTop); + const bottomCode = new Point(left, selectionBottom); + const codeHeight = selectionBottom - selectionTop; + + const codeEditDist = 50; + const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.newLines.length; + const difference = codeHeight - editHeight; + const topEdit = new Point(left + codeEditDist, selectionTop + (difference / 2)); + const bottomEdit = new Point(left + codeEditDist, selectionBottom - (difference / 2)); + + return { + topCode, + bottomCode, + codeHeight, + topEdit, + bottomEdit, + editHeight, + }; + }); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _edit: IObservable, + private readonly _userPrompt: ISettableObservable, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + const visible = derived(this, reader => this._edit.read(reader) !== undefined || this._userPrompt.read(reader) !== undefined); + this._register(applyStyle(this._elements.root, { + display: derived(this, reader => visible.read(reader) ? 'block' : 'none') + })); + + this._register(appendRemoveOnDispose(this._editor.getDomNode()!, this._elements.root)); + + this._register(observableCodeEditor(_editor).createOverlayWidget({ + domNode: this._elements.root, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: derived(reader => { + const x = this._layout1.read(reader)?.left; + if (x === undefined) { return 0; } + const width = this._previewEditorObs.contentWidth.read(reader); + return x + width; + }), + })); + + this._previewEditor.setModel(this._previewTextModel); + + this._register(this._previewEditorObs.setDecorations(this._decorations)); + + this._register(autorun(reader => { + const layoutInfo = this._layout.read(reader); + if (!layoutInfo) { return; } + + const { topCode, bottomCode, topEdit, bottomEdit, editHeight } = layoutInfo; + + const straightWidthCode = 10; + const straightWidthEdit = 0; + const bezierDist = 40; + + const path = new PathBuilder() + .moveTo(topCode) + .lineTo(topCode.deltaX(straightWidthCode)) + .curveTo( + topCode.deltaX(straightWidthCode + bezierDist), + topEdit.deltaX(-bezierDist - straightWidthEdit), + topEdit.deltaX(-straightWidthEdit), + ) + .lineTo(topEdit) + .lineTo(bottomEdit) + .lineTo(bottomEdit.deltaX(-straightWidthEdit)) + .curveTo( + bottomEdit.deltaX(-bezierDist - straightWidthEdit), + bottomCode.deltaX(straightWidthCode + bezierDist), + bottomCode.deltaX(straightWidthCode), + ) + .lineTo(bottomCode) + .build(); + + + this._elements.path.setAttribute('d', path); + + this._elements.editorContainer.style.top = `${topEdit.y}px`; + this._elements.editorContainer.style.left = `${topEdit.x}px`; + this._elements.editorContainer.style.height = `${editHeight}px`; + + const width = this._previewEditorObs.contentWidth.read(reader); + this._previewEditor.layout({ height: editHeight, width }); + })); + + this._promptEditor.setModel(this._promptTextModel); + this._promptEditor.layout(); + this._register(createTwoWaySync(mapSettableObservable(this._userPrompt, v => v ?? '', v => v), observableCodeEditor(this._promptEditor).value)); + + this._register(autorun(reader => { + const isFocused = observableCodeEditor(this._promptEditor).isFocused.read(reader); + this._elements.root.classList.toggle('focused', isFocused); + })); + } +} + +function mapSettableObservable(obs: ISettableObservable, fn1: (value: T) => T1, fn2: (value: T1) => T): ISettableObservable { + return derivedWithSetter(undefined, reader => fn1(obs.read(reader)), (value, tx) => obs.set(fn2(value), tx)); +} + +class Point { + constructor( + public readonly x: number, + public readonly y: number, + ) { } + + public add(other: Point): Point { + return new Point(this.x + other.x, this.y + other.y); + } + + public deltaX(delta: number): Point { + return new Point(this.x + delta, this.y); + } +} + +class PathBuilder { + private _data: string = ''; + + public moveTo(point: Point): this { + this._data += `M ${point.x} ${point.y} `; + return this; + } + + public lineTo(point: Point): this { + this._data += `L ${point.x} ${point.y} `; + return this; + } + + public curveTo(cp1: Point, cp2: Point, to: Point): this { + this._data += `C ${cp1.x} ${cp1.y} ${cp2.x} ${cp2.y} ${to.x} ${to.y} `; + return this; + } + + public build(): string { + return this._data; + } +} + +function createTwoWaySync(main: ISettableObservable, target: ISettableObservable): IDisposable { + const store = new DisposableStore(); + store.add(autorun(reader => { + const value = main.read(reader); + target.set(value, undefined); + })); + store.add(autorun(reader => { + const value = target.read(reader); + main.set(value, undefined); + })); + return store; +} diff --git a/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts b/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts index db892ea676c..25f273c1262 100644 --- a/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts +++ b/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { CancelablePromise, disposableTimeout } from 'vs/base/common/async'; +import { disposableTimeout } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { noBreakWhitespace } from 'vs/base/common/strings'; @@ -114,13 +114,13 @@ export class InlineProgressManager extends Disposable { private readonly _showPromise = this._register(new MutableDisposable()); private readonly _currentDecorations: IEditorDecorationsCollection; - private readonly _currentWidget = new MutableDisposable(); + private readonly _currentWidget = this._register(new MutableDisposable()); private _operationIdPool = 0; private _currentOperation?: number; constructor( - readonly id: string, + private readonly id: string, private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { @@ -129,7 +129,12 @@ export class InlineProgressManager extends Disposable { this._currentDecorations = _editor.createDecorationsCollection(); } - public async showWhile(position: IPosition, title: string, promise: CancelablePromise): Promise { + public override dispose(): void { + super.dispose(); + this._currentDecorations.clear(); + } + + public async showWhile(position: IPosition, title: string, promise: Promise, delegate: InlineProgressDelegate, delayOverride?: number): Promise { const operationId = this._operationIdPool++; this._currentOperation = operationId; @@ -143,9 +148,9 @@ export class InlineProgressManager extends Disposable { }]); if (decorationIds.length > 0) { - this._currentWidget.value = this._instantiationService.createInstance(InlineProgressWidget, this.id, this._editor, range, title, promise); + this._currentWidget.value = this._instantiationService.createInstance(InlineProgressWidget, this.id, this._editor, range, title, delegate); } - }, this._showDelay); + }, delayOverride ?? this._showDelay); try { return await promise; diff --git a/src/vs/editor/contrib/lineSelection/test/browser/lineSelection.test.ts b/src/vs/editor/contrib/lineSelection/test/browser/lineSelection.test.ts index 4ef8e7d2cfc..17b47434db7 100644 --- a/src/vs/editor/contrib/lineSelection/test/browser/lineSelection.test.ts +++ b/src/vs/editor/contrib/lineSelection/test/browser/lineSelection.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction } from 'vs/editor/browser/editorExtensions'; diff --git a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts index 45b3fafba71..60601d1fcdd 100644 --- a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts @@ -11,6 +11,7 @@ import { ReplaceCommand, ReplaceCommandThatPreservesSelection, ReplaceCommandTha import { TrimTrailingWhitespaceCommand } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { TypeOperations } from 'vs/editor/common/cursor/cursorTypeOperations'; +import { EnterOperation } from 'vs/editor/common/cursor/cursorTypeEditOperations'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -594,7 +595,7 @@ export class InsertLineBeforeAction extends EditorAction { return; } editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.lineInsertBefore(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); + editor.executeCommands(this.id, EnterOperation.lineInsertBefore(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); } } @@ -619,7 +620,7 @@ export class InsertLineAfterAction extends EditorAction { return; } editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.lineInsertAfter(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); + editor.executeCommands(this.id, EnterOperation.lineInsertAfter(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); } } diff --git a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts index 68614a2f432..9a4f6800938 100644 --- a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts @@ -42,6 +42,13 @@ export class MoveLinesCommand implements ICommand { public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { + const getLanguageId = () => { + return model.getLanguageId(); + }; + const getLanguageIdAtPosition = (lineNumber: number, column: number) => { + return model.getLanguageIdAtPosition(lineNumber, column); + }; + const modelLineCount = model.getLineCount(); if (this._isMovingDown && this._selection.endLineNumber === modelLineCount) { @@ -63,20 +70,6 @@ export class MoveLinesCommand implements ICommand { const { tabSize, indentSize, insertSpaces } = model.getOptions(); const indentConverter = this.buildIndentConverter(tabSize, indentSize, insertSpaces); - const virtualModel: IVirtualModel = { - tokenization: { - getLineTokens: (lineNumber: number) => { - return model.tokenization.getLineTokens(lineNumber); - }, - getLanguageId: () => { - return model.getLanguageId(); - }, - getLanguageIdAtPosition: (lineNumber: number, column: number) => { - return model.getLanguageIdAtPosition(lineNumber, column); - }, - }, - getLineContent: null as unknown as (lineNumber: number) => string, - }; if (s.startLineNumber === s.endLineNumber && model.getLineMaxColumn(s.startLineNumber) === 1) { // Current line is empty @@ -120,12 +113,25 @@ export class MoveLinesCommand implements ICommand { insertingText = newIndentation + this.trimStart(movingLineText); } else { // no enter rule matches, let's check indentatin rules then. - virtualModel.getLineContent = (lineNumber: number) => { - if (lineNumber === s.startLineNumber) { - return model.getLineContent(movingLineNumber); - } else { - return model.getLineContent(lineNumber); - } + const virtualModel: IVirtualModel = { + tokenization: { + getLineTokens: (lineNumber: number) => { + if (lineNumber === s.startLineNumber) { + return model.tokenization.getLineTokens(movingLineNumber); + } else { + return model.tokenization.getLineTokens(lineNumber); + } + }, + getLanguageId, + getLanguageIdAtPosition, + }, + getLineContent: (lineNumber: number) => { + if (lineNumber === s.startLineNumber) { + return model.getLineContent(movingLineNumber); + } else { + return model.getLineContent(lineNumber); + } + }, }; const indentOfMovingLine = getGoodIndentForLine( this._autoIndent, @@ -159,14 +165,30 @@ export class MoveLinesCommand implements ICommand { } } else { // it doesn't match onEnter rules, let's check indentation rules then. - virtualModel.getLineContent = (lineNumber: number) => { - if (lineNumber === s.startLineNumber) { - return insertingText; - } else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) { - return model.getLineContent(lineNumber - 1); - } else { - return model.getLineContent(lineNumber); - } + const virtualModel: IVirtualModel = { + tokenization: { + getLineTokens: (lineNumber: number) => { + if (lineNumber === s.startLineNumber) { + // TODO@aiday-mar: the tokens here don't correspond exactly to the corresponding content (after indentation adjustment), have to fix this. + return model.tokenization.getLineTokens(movingLineNumber); + } else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) { + return model.tokenization.getLineTokens(lineNumber - 1); + } else { + return model.tokenization.getLineTokens(lineNumber); + } + }, + getLanguageId, + getLanguageIdAtPosition, + }, + getLineContent: (lineNumber: number) => { + if (lineNumber === s.startLineNumber) { + return insertingText; + } else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) { + return model.getLineContent(lineNumber - 1); + } else { + return model.getLineContent(lineNumber); + } + }, }; const newIndentatOfMovingBlock = getGoodIndentForLine( @@ -204,12 +226,25 @@ export class MoveLinesCommand implements ICommand { builder.addEditOperation(new Range(s.endLineNumber, model.getLineMaxColumn(s.endLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)), '\n' + movingLineText); if (this.shouldAutoIndent(model, s)) { - virtualModel.getLineContent = (lineNumber: number) => { - if (lineNumber === movingLineNumber) { - return model.getLineContent(s.startLineNumber); - } else { - return model.getLineContent(lineNumber); - } + const virtualModel: IVirtualModel = { + tokenization: { + getLineTokens: (lineNumber: number) => { + if (lineNumber === movingLineNumber) { + return model.tokenization.getLineTokens(s.startLineNumber); + } else { + return model.tokenization.getLineTokens(lineNumber); + } + }, + getLanguageId, + getLanguageIdAtPosition, + }, + getLineContent: (lineNumber: number) => { + if (lineNumber === movingLineNumber) { + return model.getLineContent(s.startLineNumber); + } else { + return model.getLineContent(lineNumber); + } + }, }; const ret = this.matchEnterRule(model, indentConverter, tabSize, s.startLineNumber, s.startLineNumber - 2); diff --git a/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts index c53496fb5de..48348ae9690 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Selection } from 'vs/editor/common/core/selection'; import { CopyLinesCommand } from 'vs/editor/contrib/linesOperations/browser/copyLinesCommand'; diff --git a/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts index 5425697a2e4..f99fb62422d 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts b/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts index 982dc07426b..80d6ed93730 100644 --- a/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts +++ b/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; diff --git a/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts b/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts index 64de23fce27..008150e7c09 100644 --- a/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts +++ b/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts index f5b8af528e3..3036277c448 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts @@ -387,4 +387,4 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } } -registerColor('editorHoverWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hcDark: listHighlightForeground, hcLight: listHighlightForeground }, nls.localize('editorHoverWidgetHighlightForeground', 'Foreground color of the active item in the parameter hint.')); +registerColor('editorHoverWidget.highlightForeground', listHighlightForeground, nls.localize('editorHoverWidgetHighlightForeground', 'Foreground color of the active item in the parameter hint.')); diff --git a/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts b/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts index 6c84215bc72..10372b408c4 100644 --- a/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts +++ b/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { promiseWithResolvers } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/peekView/browser/peekView.ts b/src/vs/editor/contrib/peekView/browser/peekView.ts index a0f2dfd914e..86f6cb1d47f 100644 --- a/src/vs/editor/contrib/peekView/browser/peekView.ts +++ b/src/vs/editor/contrib/peekView/browser/peekView.ts @@ -289,8 +289,8 @@ export const peekViewResultsFileForeground = registerColor('peekViewResult.fileF export const peekViewResultsSelectionBackground = registerColor('peekViewResult.selectionBackground', { dark: '#3399ff33', light: '#3399ff33', hcDark: null, hcLight: null }, nls.localize('peekViewResultsSelectionBackground', 'Background color of the selected entry in the peek view result list.')); export const peekViewResultsSelectionForeground = registerColor('peekViewResult.selectionForeground', { dark: Color.white, light: '#6C6C6C', hcDark: Color.white, hcLight: editorForeground }, nls.localize('peekViewResultsSelectionForeground', 'Foreground color of the selected entry in the peek view result list.')); export const peekViewEditorBackground = registerColor('peekViewEditor.background', { dark: '#001F33', light: '#F2F8FC', hcDark: Color.black, hcLight: Color.white }, nls.localize('peekViewEditorBackground', 'Background color of the peek view editor.')); -export const peekViewEditorGutterBackground = registerColor('peekViewEditorGutter.background', { dark: peekViewEditorBackground, light: peekViewEditorBackground, hcDark: peekViewEditorBackground, hcLight: peekViewEditorBackground }, nls.localize('peekViewEditorGutterBackground', 'Background color of the gutter in the peek view editor.')); -export const peekViewEditorStickyScrollBackground = registerColor('peekViewEditorStickyScroll.background', { dark: peekViewEditorBackground, light: peekViewEditorBackground, hcDark: peekViewEditorBackground, hcLight: peekViewEditorBackground }, nls.localize('peekViewEditorStickScrollBackground', 'Background color of sticky scroll in the peek view editor.')); +export const peekViewEditorGutterBackground = registerColor('peekViewEditorGutter.background', peekViewEditorBackground, nls.localize('peekViewEditorGutterBackground', 'Background color of the gutter in the peek view editor.')); +export const peekViewEditorStickyScrollBackground = registerColor('peekViewEditorStickyScroll.background', peekViewEditorBackground, nls.localize('peekViewEditorStickScrollBackground', 'Background color of sticky scroll in the peek view editor.')); export const peekViewResultsMatchHighlight = registerColor('peekViewResult.matchHighlightBackground', { dark: '#ea5c004d', light: '#ea5c004d', hcDark: null, hcLight: null }, nls.localize('peekViewResultsMatchHighlight', 'Match highlight color in the peek view result list.')); export const peekViewEditorMatchHighlight = registerColor('peekViewEditor.matchHighlightBackground', { dark: '#ff8f0099', light: '#f5d802de', hcDark: null, hcLight: null }, nls.localize('peekViewEditorMatchHighlight', 'Match highlight color in the peek view editor.')); diff --git a/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts b/src/vs/editor/contrib/placeholderText/browser/placeholderText.contribution.ts similarity index 70% rename from src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts rename to src/vs/editor/contrib/placeholderText/browser/placeholderText.contribution.ts index 52c0edd1cb8..33c069c4485 100644 --- a/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts +++ b/src/vs/editor/contrib/placeholderText/browser/placeholderText.contribution.ts @@ -5,24 +5,31 @@ import { structuralEquals } from 'vs/base/common/equals'; import { Disposable } from 'vs/base/common/lifecycle'; -import { derived, derivedOpts, observableValue } from 'vs/base/common/observable'; +import { derived, derivedOpts } from 'vs/base/common/observable'; import 'vs/css!./placeholderText'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { obsCodeEditor } from 'vs/editor/browser/observableUtilities'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { ghostTextForeground } from 'vs/editor/common/core/editorColorRegistry'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, InjectedTextCursorStops } from 'vs/editor/common/model'; +import { localize } from 'vs/nls'; +import { registerColor } from 'vs/platform/theme/common/colorUtils'; +/** + * Use the editor option to set the placeholder text. +*/ export class PlaceholderTextContribution extends Disposable implements IEditorContribution { public static get(editor: ICodeEditor): PlaceholderTextContribution { return editor.getContribution(PlaceholderTextContribution.ID)!; } public static readonly ID = 'editor.contrib.placeholderText'; - private readonly _editorObs = obsCodeEditor(this._editor); + private readonly _editorObs = observableCodeEditor(this._editor); - private readonly _placeholderText = observableValue(this, undefined); + private readonly _placeholderText = this._editorObs.getOption(EditorOption.placeholder); private readonly _decorationOptions = derivedOpts<{ placeholder: string } | undefined>({ owner: this, equalsFn: structuralEquals }, reader => { const p = this._placeholderText.read(reader); @@ -57,10 +64,8 @@ export class PlaceholderTextContribution extends Disposable implements IEditorCo this._register(this._editorObs.setDecorations(this._decorations)); } - - public setPlaceholderText(placeholder: string): void { - this._placeholderText.set(placeholder, undefined); - } } -registerEditorContribution(PlaceholderTextContribution.ID, PlaceholderTextContribution, EditorContributionInstantiation.Lazy); +registerEditorContribution(PlaceholderTextContribution.ID, PlaceholderTextContribution, EditorContributionInstantiation.Eager); + +registerColor('editor.placeholder.foreground', { dark: ghostTextForeground, light: ghostTextForeground, hcDark: ghostTextForeground, hcLight: ghostTextForeground }, localize('placeholderForeground', 'Foreground color of the placeholder text in the editor.')); diff --git a/src/vs/editor/contrib/placeholderText/browser/placeholderText.css b/src/vs/editor/contrib/placeholderText/browser/placeholderText.css index c42b21a3ba2..3912e17fc2f 100644 --- a/src/vs/editor/contrib/placeholderText/browser/placeholderText.css +++ b/src/vs/editor/contrib/placeholderText/browser/placeholderText.css @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.monaco-editor { + --vscode-editor-placeholder-foreground: var(--vscode-editorGhostText-foreground); +} + .monaco-editor .placeholder-text { - color: var(--vscode-editorGhostText-foreground) !important; + color: var(--vscode-editor-placeholder-foreground); } diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.ts b/src/vs/editor/contrib/rename/browser/renameWidget.ts index 03257f28aa3..fc1c165a90d 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.ts +++ b/src/vs/editor/contrib/rename/browser/renameWidget.ts @@ -6,7 +6,7 @@ import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; @@ -900,7 +900,7 @@ class InputWithButton implements IDisposable { private _domNode: HTMLDivElement | undefined; private _inputNode: HTMLInputElement | undefined; private _buttonNode: HTMLElement | undefined; - private _buttonHover: IUpdatableHover | undefined; + private _buttonHover: IManagedHover | undefined; private _buttonGenHoverText: string | undefined; private _buttonCancelHoverText: string | undefined; private _sparkleIcon: HTMLElement | undefined; @@ -934,7 +934,7 @@ class InputWithButton implements IDisposable { this._buttonGenHoverText = nls.localize('generateRenameSuggestionsButton', "Generate new name suggestions"); this._buttonCancelHoverText = nls.localize('cancelRenameSuggestionsButton', "Cancel"); - this._buttonHover = getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('element'), this._buttonNode, this._buttonGenHoverText); + this._buttonHover = getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('element'), this._buttonNode, this._buttonGenHoverText); this._disposables.add(this._buttonHover); this._domNode.appendChild(this._buttonNode); diff --git a/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts b/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts index c2d9a57c202..86d3bf5d62d 100644 --- a/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts +++ b/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Barrier, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts b/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts index da8dc2f222d..6ac97ecde96 100644 --- a/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts +++ b/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { canceled } from 'vs/base/common/errors'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts index dc9e9767d81..1de9179cc28 100644 --- a/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts index e0f11350e89..236ad70f27d 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts index 1c9c3a80cdb..5b9a5e434f8 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts index ee2f1f1d24f..bbf24168409 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Choice, FormatString, Marker, Placeholder, Scanner, SnippetParser, Text, TextmateSnippet, TokenType, Transform, Variable } from 'vs/editor/contrib/snippet/browser/snippetParser'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts index 61061846724..0a9a3c76462 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts index 53a9b272757..b8ea4e52342 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { sep } from 'vs/base/common/path'; diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css index 3bc52c6c915..276082256f6 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css @@ -61,7 +61,7 @@ .monaco-editor .sticky-widget { width: 100%; - box-shadow: var(--vscode-editorStickyScroll-shadow) 0 3px 2px -2px; + box-shadow: var(--vscode-editorStickyScroll-shadow) 0 4px 2px -2px; z-index: 4; background-color: var(--vscode-editorStickyScroll-background); right: initial !important; diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts index bdc0542b18e..22b8c67a294 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts @@ -51,7 +51,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib private readonly _sessionStore: DisposableStore = new DisposableStore(); private _widgetState: StickyScrollWidgetState; - private _foldingModel: FoldingModel | null = null; + private _foldingModel: FoldingModel | undefined; private _maxStickyLines: number = Number.MAX_SAFE_INTEGER; private _stickyRangeProjectedOnEditor: IRange | undefined; @@ -67,7 +67,8 @@ export class StickyScrollController extends Disposable implements IEditorContrib private _positionRevealed = false; private _onMouseDown = false; private _endLineNumbers: number[] = []; - private _showEndForLine: number | null = null; + private _showEndForLine: number | undefined; + private _minRebuildFromLine: number | undefined; constructor( private readonly _editor: ICodeEditor, @@ -84,7 +85,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._register(this._stickyScrollWidget); this._register(this._stickyLineCandidateProvider); - this._widgetState = new StickyScrollWidgetState([], [], 0); + this._widgetState = StickyScrollWidgetState.Empty; this._onDidResize(); this._readConfiguration(); const stickyScrollDomNode = this._stickyScrollWidget.getDomNode(); @@ -291,14 +292,14 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._renderStickyScroll(); return; } - if (this._showEndForLine !== null) { - this._showEndForLine = null; + if (this._showEndForLine !== undefined) { + this._showEndForLine = undefined; this._renderStickyScroll(); } })); this._register(dom.addDisposableListener(stickyScrollWidgetDomNode, dom.EventType.MOUSE_LEAVE, (e) => { - if (this._showEndForLine !== null) { - this._showEndForLine = null; + if (this._showEndForLine !== undefined) { + this._showEndForLine = undefined; this._renderStickyScroll(); } })); @@ -415,14 +416,14 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._editor.addOverlayWidget(this._stickyScrollWidget); this._sessionStore.add(this._editor.onDidScrollChange((e) => { if (e.scrollTopChanged) { - this._showEndForLine = null; + this._showEndForLine = undefined; this._renderStickyScroll(); } })); this._sessionStore.add(this._editor.onDidLayoutChange(() => this._onDidResize())); this._sessionStore.add(this._editor.onDidChangeModelTokens((e) => this._onTokensChange(e))); this._sessionStore.add(this._stickyLineCandidateProvider.onDidChangeStickyScroll(() => { - this._showEndForLine = null; + this._showEndForLine = undefined; this._renderStickyScroll(); })); this._enabled = true; @@ -431,7 +432,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib const lineNumberOption = this._editor.getOption(EditorOption.lineNumbers); if (lineNumberOption.renderType === RenderLineNumbersType.Relative) { this._sessionStore.add(this._editor.onDidChangeCursorPosition(() => { - this._showEndForLine = null; + this._showEndForLine = undefined; this._renderStickyScroll(0); })); } @@ -479,32 +480,29 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._maxStickyLines = Math.round(theoreticalLines * .25); } - private async _renderStickyScroll(rebuildFromLine?: number) { + private async _renderStickyScroll(rebuildFromLine?: number): Promise { const model = this._editor.getModel(); if (!model || model.isTooLargeForTokenization()) { - this._foldingModel = null; - this._stickyScrollWidget.setState(undefined, null); + this._resetState(); return; } - const stickyLineVersion = this._stickyLineCandidateProvider.getVersionId(); - if (stickyLineVersion === undefined || stickyLineVersion === model.getVersionId()) { - this._foldingModel = await FoldingController.get(this._editor)?.getFoldingModel() ?? null; - this._widgetState = this.findScrollWidgetState(); - this._stickyScrollVisibleContextKey.set(!(this._widgetState.startLineNumbers.length === 0)); - + const nextRebuildFromLine = this._updateAndGetMinRebuildFromLine(rebuildFromLine); + const stickyWidgetVersion = this._stickyLineCandidateProvider.getVersionId(); + const shouldUpdateState = stickyWidgetVersion === undefined || stickyWidgetVersion === model.getVersionId(); + if (shouldUpdateState) { if (!this._focused) { - this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); + await this._updateState(nextRebuildFromLine); } else { // Suppose that previously the sticky scroll widget had height 0, then if there are visible lines, set the last line as focused if (this._focusedStickyElementIndex === -1) { - this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); + await this._updateState(nextRebuildFromLine); this._focusedStickyElementIndex = this._stickyScrollWidget.lineNumberCount - 1; if (this._focusedStickyElementIndex !== -1) { this._stickyScrollWidget.focusLineWithIndex(this._focusedStickyElementIndex); } } else { const focusedStickyElementLineNumber = this._stickyScrollWidget.lineNumbers[this._focusedStickyElementIndex]; - this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); + await this._updateState(nextRebuildFromLine); // Suppose that after setting the state, there are no sticky lines, set the focused index to -1 if (this._stickyScrollWidget.lineNumberCount === 0) { this._focusedStickyElementIndex = -1; @@ -523,6 +521,31 @@ export class StickyScrollController extends Disposable implements IEditorContrib } } + private _updateAndGetMinRebuildFromLine(rebuildFromLine: number | undefined): number | undefined { + if (rebuildFromLine !== undefined) { + const minRebuildFromLineOrInfinity = this._minRebuildFromLine !== undefined ? this._minRebuildFromLine : Infinity; + this._minRebuildFromLine = Math.min(rebuildFromLine, minRebuildFromLineOrInfinity); + } + return this._minRebuildFromLine; + } + + private async _updateState(rebuildFromLine?: number): Promise { + this._minRebuildFromLine = undefined; + this._foldingModel = await FoldingController.get(this._editor)?.getFoldingModel() ?? undefined; + this._widgetState = this.findScrollWidgetState(); + const stickyWidgetHasLines = this._widgetState.startLineNumbers.length > 0; + this._stickyScrollVisibleContextKey.set(stickyWidgetHasLines); + this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); + } + + private async _resetState(): Promise { + this._minRebuildFromLine = undefined; + this._foldingModel = undefined; + this._widgetState = StickyScrollWidgetState.Empty; + this._stickyScrollVisibleContextKey.set(false); + this._stickyScrollWidget.setState(undefined, undefined); + } + findScrollWidgetState(): StickyScrollWidgetState { const lineHeight: number = this._editor.getOption(EditorOption.lineHeight); const maxNumberStickyLines = Math.min(this._maxStickyLines, this._editor.getOption(EditorOption.stickyScroll).maxLineCount); diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index d0e8da4b17a..27d3d8e1a58 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -35,6 +35,10 @@ export class StickyScrollWidgetState { && equals(this.startLineNumbers, other.startLineNumbers) && equals(this.endLineNumbers, other.endLineNumbers); } + + static get Empty() { + return new StickyScrollWidgetState([], [], 0); + } } const _ttPolicy = createTrustedTypesPolicy('stickyScrollViewLayer', { createHTML: value => value }); @@ -126,7 +130,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { return this._lineNumbers; } - setState(_state: StickyScrollWidgetState | undefined, foldingModel: FoldingModel | null, _rebuildFromLine?: number): void { + setState(_state: StickyScrollWidgetState | undefined, foldingModel: FoldingModel | undefined, _rebuildFromLine?: number): void { if (_rebuildFromLine === undefined && ((!this._previousState && !_state) || (this._previousState && this._previousState.equals(_state))) ) { @@ -205,7 +209,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } } - private async _renderRootNode(state: StickyScrollWidgetState | undefined, foldingModel: FoldingModel | null, rebuildFromLine: number): Promise { + private async _renderRootNode(state: StickyScrollWidgetState | undefined, foldingModel: FoldingModel | undefined, rebuildFromLine: number): Promise { this._clearStickyLinesFromLine(rebuildFromLine); if (!state) { return; @@ -258,7 +262,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { })); } - private _renderChildNode(index: number, line: number, foldingModel: FoldingModel | null, layoutInfo: EditorLayoutInfo): RenderedStickyLine | undefined { + private _renderChildNode(index: number, line: number, foldingModel: FoldingModel | undefined, layoutInfo: EditorLayoutInfo): RenderedStickyLine | undefined { const viewModel = this._editor._getViewModel(); if (!viewModel) { return; @@ -358,7 +362,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { return stickyLine; } - private _renderFoldingIconForLine(foldingModel: FoldingModel | null, line: number): StickyFoldingIcon | undefined { + private _renderFoldingIconForLine(foldingModel: FoldingModel | undefined, line: number): StickyFoldingIcon | undefined { const showFoldingControls: 'mouseover' | 'always' | 'never' = this._editor.getOption(EditorOption.showFoldingControls); if (!foldingModel || showFoldingControls === 'never') { return; diff --git a/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts b/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts index 04e852765ca..aaefcb65f3d 100644 --- a/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts +++ b/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { withAsyncTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { StickyScrollController } from 'vs/editor/contrib/stickyScroll/browser/stickyScrollController'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 2eeb94d99b6..2d30e5925ab 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -39,15 +39,15 @@ import { status } from 'vs/base/browser/ui/aria/aria'; /** * Suggest widget colors */ -registerColor('editorSuggestWidget.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.')); -registerColor('editorSuggestWidget.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.')); -const editorSuggestWidgetForeground = registerColor('editorSuggestWidget.foreground', { dark: editorForeground, light: editorForeground, hcDark: editorForeground, hcLight: editorForeground }, nls.localize('editorSuggestWidgetForeground', 'Foreground color of the suggest widget.')); -registerColor('editorSuggestWidget.selectedForeground', { dark: quickInputListFocusForeground, light: quickInputListFocusForeground, hcDark: quickInputListFocusForeground, hcLight: quickInputListFocusForeground }, nls.localize('editorSuggestWidgetSelectedForeground', 'Foreground color of the selected entry in the suggest widget.')); -registerColor('editorSuggestWidget.selectedIconForeground', { dark: quickInputListFocusIconForeground, light: quickInputListFocusIconForeground, hcDark: quickInputListFocusIconForeground, hcLight: quickInputListFocusIconForeground }, nls.localize('editorSuggestWidgetSelectedIconForeground', 'Icon foreground color of the selected entry in the suggest widget.')); -export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: quickInputListFocusBackground, light: quickInputListFocusBackground, hcDark: quickInputListFocusBackground, hcLight: quickInputListFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); -registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hcDark: listHighlightForeground, hcLight: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); -registerColor('editorSuggestWidget.focusHighlightForeground', { dark: listFocusHighlightForeground, light: listFocusHighlightForeground, hcDark: listFocusHighlightForeground, hcLight: listFocusHighlightForeground }, nls.localize('editorSuggestWidgetFocusHighlightForeground', 'Color of the match highlights in the suggest widget when an item is focused.')); -registerColor('editorSuggestWidgetStatus.foreground', { dark: transparent(editorSuggestWidgetForeground, .5), light: transparent(editorSuggestWidgetForeground, .5), hcDark: transparent(editorSuggestWidgetForeground, .5), hcLight: transparent(editorSuggestWidgetForeground, .5) }, nls.localize('editorSuggestWidgetStatusForeground', 'Foreground color of the suggest widget status.')); +registerColor('editorSuggestWidget.background', editorWidgetBackground, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.')); +registerColor('editorSuggestWidget.border', editorWidgetBorder, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.')); +const editorSuggestWidgetForeground = registerColor('editorSuggestWidget.foreground', editorForeground, nls.localize('editorSuggestWidgetForeground', 'Foreground color of the suggest widget.')); +registerColor('editorSuggestWidget.selectedForeground', quickInputListFocusForeground, nls.localize('editorSuggestWidgetSelectedForeground', 'Foreground color of the selected entry in the suggest widget.')); +registerColor('editorSuggestWidget.selectedIconForeground', quickInputListFocusIconForeground, nls.localize('editorSuggestWidgetSelectedIconForeground', 'Icon foreground color of the selected entry in the suggest widget.')); +export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', quickInputListFocusBackground, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); +registerColor('editorSuggestWidget.highlightForeground', listHighlightForeground, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); +registerColor('editorSuggestWidget.focusHighlightForeground', listFocusHighlightForeground, nls.localize('editorSuggestWidgetFocusHighlightForeground', 'Color of the match highlights in the suggest widget when an item is focused.')); +registerColor('editorSuggestWidgetStatus.foreground', transparent(editorSuggestWidgetForeground, .5), nls.localize('editorSuggestWidgetStatusForeground', 'Foreground color of the suggest widget status.')); const enum State { Hidden, diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts b/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts index 25d19c054d2..4a1df5b9ce8 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts @@ -6,31 +6,12 @@ import * as dom from 'vs/base/browser/dom'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction } from 'vs/base/common/actions'; -import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { localize } from 'vs/nls'; -import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { TextOnlyMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -class StatusBarViewItem extends MenuEntryActionViewItem { - - protected override updateLabel() { - const kb = this._keybindingService.lookupKeybinding(this._action.id, this._contextKeyService); - if (!kb) { - return super.updateLabel(); - } - if (this.label) { - this.label.textContent = localize({ key: 'content', comment: ['A label', 'A keybinding'] }, '{0} ({1})', this._action.label, StatusBarViewItem.symbolPrintEnter(kb)); - } - } - - static symbolPrintEnter(kb: ResolvedKeybinding) { - return kb.getLabel()?.replace(/\benter\b/gi, '\u23CE'); - } -} - export class SuggestWidgetStatus { readonly element: HTMLElement; @@ -49,7 +30,7 @@ export class SuggestWidgetStatus { this.element = dom.append(container, dom.$('.suggest-status-bar')); const actionViewItemProvider = (action => { - return action instanceof MenuItemAction ? instantiationService.createInstance(StatusBarViewItem, action, undefined) : undefined; + return action instanceof MenuItemAction ? instantiationService.createInstance(TextOnlyMenuEntryActionViewItem, action, { useComma: true }) : undefined; }); this._leftActions = new ActionBar(this.element, { actionViewItemProvider }); this._rightActions = new ActionBar(this.element, { actionViewItemProvider }); diff --git a/src/vs/editor/contrib/suggest/test/browser/completionModel.test.ts b/src/vs/editor/contrib/suggest/test/browser/completionModel.test.ts index cb08e56fb7f..823d6865a84 100644 --- a/src/vs/editor/contrib/suggest/test/browser/completionModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/completionModel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorOptions, InternalSuggestOptions } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts index d4df28ea332..b2e9fc03577 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestController.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestController.test.ts index 617019ff5cf..64123de47db 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestController.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestController.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts index 1aaca84d484..91d42374b1f 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestMemory.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestMemory.test.ts index 3bbc8c58838..57d0d030d48 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestMemory.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestMemory.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IPosition } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts index a5ded1ce5df..43abffb6425 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts b/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts index 04f7bce1cc1..15f238b1627 100644 --- a/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts b/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts index 64ccb1bc618..7d6fe4b73f6 100644 --- a/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts +++ b/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts @@ -7,19 +7,9 @@ import 'vs/css!./symbolIcons'; import { localize } from 'vs/nls'; import { foreground, registerColor } from 'vs/platform/theme/common/colorRegistry'; -export const SYMBOL_ICON_ARRAY_FOREGROUND = registerColor('symbolIcon.arrayForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground, -}, localize('symbolIcon.arrayForeground', 'The foreground color for array symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_BOOLEAN_FOREGROUND = registerColor('symbolIcon.booleanForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground, -}, localize('symbolIcon.booleanForeground', 'The foreground color for boolean symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_ARRAY_FOREGROUND = registerColor('symbolIcon.arrayForeground', foreground, localize('symbolIcon.arrayForeground', 'The foreground color for array symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_BOOLEAN_FOREGROUND = registerColor('symbolIcon.booleanForeground', foreground, localize('symbolIcon.booleanForeground', 'The foreground color for boolean symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_CLASS_FOREGROUND = registerColor('symbolIcon.classForeground', { dark: '#EE9D28', @@ -28,19 +18,9 @@ export const SYMBOL_ICON_CLASS_FOREGROUND = registerColor('symbolIcon.classForeg hcLight: '#D67E00' }, localize('symbolIcon.classForeground', 'The foreground color for class symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_COLOR_FOREGROUND = registerColor('symbolIcon.colorForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.colorForeground', 'The foreground color for color symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_COLOR_FOREGROUND = registerColor('symbolIcon.colorForeground', foreground, localize('symbolIcon.colorForeground', 'The foreground color for color symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_CONSTANT_FOREGROUND = registerColor('symbolIcon.constantForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.constantForeground', 'The foreground color for constant symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_CONSTANT_FOREGROUND = registerColor('symbolIcon.constantForeground', foreground, localize('symbolIcon.constantForeground', 'The foreground color for constant symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_CONSTRUCTOR_FOREGROUND = registerColor('symbolIcon.constructorForeground', { dark: '#B180D7', @@ -77,19 +57,9 @@ export const SYMBOL_ICON_FIELD_FOREGROUND = registerColor('symbolIcon.fieldForeg hcLight: '#007ACC' }, localize('symbolIcon.fieldForeground', 'The foreground color for field symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_FILE_FOREGROUND = registerColor('symbolIcon.fileForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.fileForeground', 'The foreground color for file symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_FILE_FOREGROUND = registerColor('symbolIcon.fileForeground', foreground, localize('symbolIcon.fileForeground', 'The foreground color for file symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_FOLDER_FOREGROUND = registerColor('symbolIcon.folderForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.folderForeground', 'The foreground color for folder symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_FOLDER_FOREGROUND = registerColor('symbolIcon.folderForeground', foreground, localize('symbolIcon.folderForeground', 'The foreground color for folder symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_FUNCTION_FOREGROUND = registerColor('symbolIcon.functionForeground', { dark: '#B180D7', @@ -105,19 +75,9 @@ export const SYMBOL_ICON_INTERFACE_FOREGROUND = registerColor('symbolIcon.interf hcLight: '#007ACC' }, localize('symbolIcon.interfaceForeground', 'The foreground color for interface symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_KEY_FOREGROUND = registerColor('symbolIcon.keyForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.keyForeground', 'The foreground color for key symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_KEY_FOREGROUND = registerColor('symbolIcon.keyForeground', foreground, localize('symbolIcon.keyForeground', 'The foreground color for key symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_KEYWORD_FOREGROUND = registerColor('symbolIcon.keywordForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.keywordForeground', 'The foreground color for keyword symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_KEYWORD_FOREGROUND = registerColor('symbolIcon.keywordForeground', foreground, localize('symbolIcon.keywordForeground', 'The foreground color for keyword symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_METHOD_FOREGROUND = registerColor('symbolIcon.methodForeground', { dark: '#B180D7', @@ -126,110 +86,35 @@ export const SYMBOL_ICON_METHOD_FOREGROUND = registerColor('symbolIcon.methodFor hcLight: '#652D90' }, localize('symbolIcon.methodForeground', 'The foreground color for method symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_MODULE_FOREGROUND = registerColor('symbolIcon.moduleForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.moduleForeground', 'The foreground color for module symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_NAMESPACE_FOREGROUND = registerColor('symbolIcon.namespaceForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.namespaceForeground', 'The foreground color for namespace symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_NULL_FOREGROUND = registerColor('symbolIcon.nullForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.nullForeground', 'The foreground color for null symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_NUMBER_FOREGROUND = registerColor('symbolIcon.numberForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.numberForeground', 'The foreground color for number symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_OBJECT_FOREGROUND = registerColor('symbolIcon.objectForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.objectForeground', 'The foreground color for object symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_OPERATOR_FOREGROUND = registerColor('symbolIcon.operatorForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.operatorForeground', 'The foreground color for operator symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_PACKAGE_FOREGROUND = registerColor('symbolIcon.packageForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.packageForeground', 'The foreground color for package symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_PROPERTY_FOREGROUND = registerColor('symbolIcon.propertyForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.propertyForeground', 'The foreground color for property symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_REFERENCE_FOREGROUND = registerColor('symbolIcon.referenceForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.referenceForeground', 'The foreground color for reference symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_SNIPPET_FOREGROUND = registerColor('symbolIcon.snippetForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.snippetForeground', 'The foreground color for snippet symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_STRING_FOREGROUND = registerColor('symbolIcon.stringForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.stringForeground', 'The foreground color for string symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_STRUCT_FOREGROUND = registerColor('symbolIcon.structForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground, -}, localize('symbolIcon.structForeground', 'The foreground color for struct symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_TEXT_FOREGROUND = registerColor('symbolIcon.textForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.textForeground', 'The foreground color for text symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_TYPEPARAMETER_FOREGROUND = registerColor('symbolIcon.typeParameterForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.typeParameterForeground', 'The foreground color for type parameter symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_UNIT_FOREGROUND = registerColor('symbolIcon.unitForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.unitForeground', 'The foreground color for unit symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_MODULE_FOREGROUND = registerColor('symbolIcon.moduleForeground', foreground, localize('symbolIcon.moduleForeground', 'The foreground color for module symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_NAMESPACE_FOREGROUND = registerColor('symbolIcon.namespaceForeground', foreground, localize('symbolIcon.namespaceForeground', 'The foreground color for namespace symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_NULL_FOREGROUND = registerColor('symbolIcon.nullForeground', foreground, localize('symbolIcon.nullForeground', 'The foreground color for null symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_NUMBER_FOREGROUND = registerColor('symbolIcon.numberForeground', foreground, localize('symbolIcon.numberForeground', 'The foreground color for number symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_OBJECT_FOREGROUND = registerColor('symbolIcon.objectForeground', foreground, localize('symbolIcon.objectForeground', 'The foreground color for object symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_OPERATOR_FOREGROUND = registerColor('symbolIcon.operatorForeground', foreground, localize('symbolIcon.operatorForeground', 'The foreground color for operator symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_PACKAGE_FOREGROUND = registerColor('symbolIcon.packageForeground', foreground, localize('symbolIcon.packageForeground', 'The foreground color for package symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_PROPERTY_FOREGROUND = registerColor('symbolIcon.propertyForeground', foreground, localize('symbolIcon.propertyForeground', 'The foreground color for property symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_REFERENCE_FOREGROUND = registerColor('symbolIcon.referenceForeground', foreground, localize('symbolIcon.referenceForeground', 'The foreground color for reference symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_SNIPPET_FOREGROUND = registerColor('symbolIcon.snippetForeground', foreground, localize('symbolIcon.snippetForeground', 'The foreground color for snippet symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_STRING_FOREGROUND = registerColor('symbolIcon.stringForeground', foreground, localize('symbolIcon.stringForeground', 'The foreground color for string symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_STRUCT_FOREGROUND = registerColor('symbolIcon.structForeground', foreground, localize('symbolIcon.structForeground', 'The foreground color for struct symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_TEXT_FOREGROUND = registerColor('symbolIcon.textForeground', foreground, localize('symbolIcon.textForeground', 'The foreground color for text symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_TYPEPARAMETER_FOREGROUND = registerColor('symbolIcon.typeParameterForeground', foreground, localize('symbolIcon.typeParameterForeground', 'The foreground color for type parameter symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); + +export const SYMBOL_ICON_UNIT_FOREGROUND = registerColor('symbolIcon.unitForeground', foreground, localize('symbolIcon.unitForeground', 'The foreground color for unit symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_VARIABLE_FOREGROUND = registerColor('symbolIcon.variableForeground', { dark: '#75BEFF', diff --git a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts index f44fc76e0bd..eae47ac2d71 100644 --- a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts +++ b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts @@ -7,7 +7,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { CharCode } from 'vs/base/common/charCode'; import { Codicon } from 'vs/base/common/codicons'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { InvisibleCharacters, isBasicASCII } from 'vs/base/common/strings'; import 'vs/css!./unicodeHighlighter'; @@ -22,7 +22,7 @@ import { UnicodeHighlighterOptions, UnicodeHighlighterReason, UnicodeHighlighter import { IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { isModelDecorationInComment, isModelDecorationInString, isModelDecorationVisible } from 'vs/editor/common/viewModel/viewModelDecorations'; -import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { MarkdownHover, renderMarkdownHovers } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; import { BannerController } from 'vs/editor/contrib/unicodeHighlighter/browser/bannerController'; import * as nls from 'vs/nls'; @@ -506,9 +506,13 @@ export class UnicodeHighlighterHoverParticipant implements IEditorHoverParticipa return result; } - public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: MarkdownHover[]): IDisposable { + public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: MarkdownHover[]): IRenderedHoverParts { return renderMarkdownHovers(context, hoverParts, this._editor, this._languageService, this._openerService); } + + public getAccessibleContent(hoverPart: MarkdownHover): string { + return hoverPart.contents.map(c => c.value).join('\n'); + } } function codePointToHex(codePoint: number): string { diff --git a/src/vs/editor/contrib/wordHighlighter/browser/highlightDecorations.ts b/src/vs/editor/contrib/wordHighlighter/browser/highlightDecorations.ts index b3825b0f05d..79ec7ad02e6 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/highlightDecorations.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/highlightDecorations.ts @@ -13,13 +13,13 @@ import { registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/ const wordHighlightBackground = registerColor('editor.wordHighlightBackground', { dark: '#575757B8', light: '#57575740', hcDark: null, hcLight: null }, nls.localize('wordHighlight', 'Background color of a symbol during read-access, like reading a variable. The color must not be opaque so as not to hide underlying decorations.'), true); registerColor('editor.wordHighlightStrongBackground', { dark: '#004972B8', light: '#0e639c40', hcDark: null, hcLight: null }, nls.localize('wordHighlightStrong', 'Background color of a symbol during write-access, like writing to a variable. The color must not be opaque so as not to hide underlying decorations.'), true); -registerColor('editor.wordHighlightTextBackground', { light: wordHighlightBackground, dark: wordHighlightBackground, hcDark: wordHighlightBackground, hcLight: wordHighlightBackground }, nls.localize('wordHighlightText', 'Background color of a textual occurrence for a symbol. The color must not be opaque so as not to hide underlying decorations.'), true); +registerColor('editor.wordHighlightTextBackground', wordHighlightBackground, nls.localize('wordHighlightText', 'Background color of a textual occurrence for a symbol. The color must not be opaque so as not to hide underlying decorations.'), true); const wordHighlightBorder = registerColor('editor.wordHighlightBorder', { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('wordHighlightBorder', 'Border color of a symbol during read-access, like reading a variable.')); registerColor('editor.wordHighlightStrongBorder', { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('wordHighlightStrongBorder', 'Border color of a symbol during write-access, like writing to a variable.')); -registerColor('editor.wordHighlightTextBorder', { light: wordHighlightBorder, dark: wordHighlightBorder, hcDark: wordHighlightBorder, hcLight: wordHighlightBorder }, nls.localize('wordHighlightTextBorder', "Border color of a textual occurrence for a symbol.")); -const overviewRulerWordHighlightForeground = registerColor('editorOverviewRuler.wordHighlightForeground', { dark: '#A0A0A0CC', light: '#A0A0A0CC', hcDark: '#A0A0A0CC', hcLight: '#A0A0A0CC' }, nls.localize('overviewRulerWordHighlightForeground', 'Overview ruler marker color for symbol highlights. The color must not be opaque so as not to hide underlying decorations.'), true); -const overviewRulerWordHighlightStrongForeground = registerColor('editorOverviewRuler.wordHighlightStrongForeground', { dark: '#C0A0C0CC', light: '#C0A0C0CC', hcDark: '#C0A0C0CC', hcLight: '#C0A0C0CC' }, nls.localize('overviewRulerWordHighlightStrongForeground', 'Overview ruler marker color for write-access symbol highlights. The color must not be opaque so as not to hide underlying decorations.'), true); -const overviewRulerWordHighlightTextForeground = registerColor('editorOverviewRuler.wordHighlightTextForeground', { dark: overviewRulerSelectionHighlightForeground, light: overviewRulerSelectionHighlightForeground, hcDark: overviewRulerSelectionHighlightForeground, hcLight: overviewRulerSelectionHighlightForeground }, nls.localize('overviewRulerWordHighlightTextForeground', 'Overview ruler marker color of a textual occurrence for a symbol. The color must not be opaque so as not to hide underlying decorations.'), true); +registerColor('editor.wordHighlightTextBorder', wordHighlightBorder, nls.localize('wordHighlightTextBorder', "Border color of a textual occurrence for a symbol.")); +const overviewRulerWordHighlightForeground = registerColor('editorOverviewRuler.wordHighlightForeground', '#A0A0A0CC', nls.localize('overviewRulerWordHighlightForeground', 'Overview ruler marker color for symbol highlights. The color must not be opaque so as not to hide underlying decorations.'), true); +const overviewRulerWordHighlightStrongForeground = registerColor('editorOverviewRuler.wordHighlightStrongForeground', '#C0A0C0CC', nls.localize('overviewRulerWordHighlightStrongForeground', 'Overview ruler marker color for write-access symbol highlights. The color must not be opaque so as not to hide underlying decorations.'), true); +const overviewRulerWordHighlightTextForeground = registerColor('editorOverviewRuler.wordHighlightTextForeground', overviewRulerSelectionHighlightForeground, nls.localize('overviewRulerWordHighlightTextForeground', 'Overview ruler marker color of a textual occurrence for a symbol. The color must not be opaque so as not to hide underlying decorations.'), true); const _WRITE_OPTIONS = ModelDecorationOptions.register({ description: 'word-highlight-strong', diff --git a/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts b/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts index 388d42021d2..29381ecf63f 100644 --- a/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts @@ -48,10 +48,10 @@ export abstract class MoveWordCommand extends EditorCommand { const wordSeparators = getMapForWordSeparators(editor.getOption(EditorOption.wordSeparators), editor.getOption(EditorOption.wordSegmenterLocales)); const model = editor.getModel(); const selections = editor.getSelections(); - + const hasMulticursor = selections.length > 1; const result = selections.map((sel) => { const inPosition = new Position(sel.positionLineNumber, sel.positionColumn); - const outPosition = this._move(wordSeparators, model, inPosition, this._wordNavigationType); + const outPosition = this._move(wordSeparators, model, inPosition, this._wordNavigationType, hasMulticursor); return this._moveTo(sel, outPosition, this._inSelectionMode); }); @@ -83,17 +83,17 @@ export abstract class MoveWordCommand extends EditorCommand { } } - protected abstract _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position; + protected abstract _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position; } export class WordLeftCommand extends MoveWordCommand { - protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return WordOperations.moveWordLeft(wordSeparators, model, position, wordNavigationType); + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return WordOperations.moveWordLeft(wordSeparators, model, position, wordNavigationType, hasMulticursor); } } export class WordRightCommand extends MoveWordCommand { - protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { return WordOperations.moveWordRight(wordSeparators, model, position, wordNavigationType); } } @@ -187,8 +187,8 @@ export class CursorWordAccessibilityLeft extends WordLeftCommand { }); } - protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType, hasMulticursor); } } @@ -202,8 +202,8 @@ export class CursorWordAccessibilityLeftSelect extends WordLeftCommand { }); } - protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType, hasMulticursor); } } @@ -295,8 +295,8 @@ export class CursorWordAccessibilityRight extends WordRightCommand { }); } - protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType, hasMulticursor); } } @@ -310,8 +310,8 @@ export class CursorWordAccessibilityRightSelect extends WordRightCommand { }); } - protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType, hasMulticursor); } } diff --git a/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts index a06bf07200a..51cdccc4fd3 100644 --- a/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; @@ -217,6 +217,40 @@ suite('WordOperations', () => { assert.deepStrictEqual(actual, EXPECTED); }); + test('cursorWordLeft - issue #169904: cursors out of sync', () => { + const text = [ + '.grid1 {', + ' display: grid;', + ' grid-template-columns:', + ' [full-start] minmax(1em, 1fr)', + ' [main-start] minmax(0, 40em) [main-end]', + ' minmax(1em, 1fr) [full-end];', + '}', + '.grid2 {', + ' display: grid;', + ' grid-template-columns:', + ' [full-start] minmax(1em, 1fr)', + ' [main-start] minmax(0, 40em) [main-end] minmax(1em, 1fr) [full-end];', + '}', + ]; + withTestCodeEditor(text, {}, (editor) => { + editor.setSelections([ + new Selection(5, 44, 5, 44), + new Selection(6, 32, 6, 32), + new Selection(12, 44, 12, 44), + new Selection(12, 72, 12, 72), + ]); + cursorWordLeft(editor, false); + assert.deepStrictEqual(editor.getSelections(), [ + new Selection(5, 43, 5, 43), + new Selection(6, 31, 6, 31), + new Selection(12, 43, 12, 43), + new Selection(12, 71, 12, 71), + ]); + + }); + }); + test('cursorWordLeftSelect - issue #74369: cursorWordLeft and cursorWordLeftSelect do not behave consistently', () => { const EXPECTED = [ '|this.|is.|a.|test', diff --git a/src/vs/editor/contrib/wordPartOperations/browser/wordPartOperations.ts b/src/vs/editor/contrib/wordPartOperations/browser/wordPartOperations.ts index ebcabc415a4..5ed4d310941 100644 --- a/src/vs/editor/contrib/wordPartOperations/browser/wordPartOperations.ts +++ b/src/vs/editor/contrib/wordPartOperations/browser/wordPartOperations.ts @@ -68,8 +68,8 @@ export class DeleteWordPartRight extends DeleteWordCommand { } export class WordPartLeftCommand extends MoveWordCommand { - protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return WordPartOperations.moveWordPartLeft(wordSeparators, model, position); + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return WordPartOperations.moveWordPartLeft(wordSeparators, model, position, hasMulticursor); } } export class CursorWordPartLeft extends WordPartLeftCommand { @@ -111,7 +111,7 @@ export class CursorWordPartLeftSelect extends WordPartLeftCommand { CommandsRegistry.registerCommandAlias('cursorWordPartStartLeftSelect', 'cursorWordPartLeftSelect'); export class WordPartRightCommand extends MoveWordCommand { - protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { return WordPartOperations.moveWordPartRight(wordSeparators, model, position); } } diff --git a/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts b/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts index 12258f71cfb..b3a72759a04 100644 --- a/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts +++ b/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorCommand } from 'vs/editor/browser/editorExtensions'; diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 362969d2b1d..b8650497f32 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -42,7 +42,9 @@ import 'vs/editor/contrib/links/browser/links'; import 'vs/editor/contrib/longLinesHelper/browser/longLinesHelper'; import 'vs/editor/contrib/multicursor/browser/multicursor'; import 'vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution'; +import 'vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution'; import 'vs/editor/contrib/parameterHints/browser/parameterHints'; +import 'vs/editor/contrib/placeholderText/browser/placeholderText.contribution'; import 'vs/editor/contrib/rename/browser/rename'; import 'vs/editor/contrib/sectionHeaders/browser/sectionHeaders'; import 'vs/editor/contrib/semanticTokens/browser/documentSemanticTokens'; diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 5e15b129e0e..9682a3cfb0f 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -251,7 +251,7 @@ class StandaloneDialogService implements IDialogService { return { confirmed, checkboxChecked: false // unsupported - } as IConfirmationResult; + }; } private doConfirm(message: string, detail?: string): boolean { diff --git a/src/vs/editor/standalone/common/monarch/monarchCompile.ts b/src/vs/editor/standalone/common/monarch/monarchCompile.ts index e9f5f3934c4..599c89a0213 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCompile.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCompile.ts @@ -434,21 +434,21 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm } // Create our lexer - const lexer: monarchCommon.ILexer = {}; - lexer.languageId = languageId; - lexer.includeLF = bool(json.includeLF, false); - lexer.noThrow = false; // raise exceptions during compilation - lexer.maxStack = 100; - - // Set standard fields: be defensive about types - lexer.start = (typeof json.start === 'string' ? json.start : null); - lexer.ignoreCase = bool(json.ignoreCase, false); - lexer.unicode = bool(json.unicode, false); - - lexer.tokenPostfix = string(json.tokenPostfix, '.' + lexer.languageId); - lexer.defaultToken = string(json.defaultToken, 'source'); - - lexer.usesEmbedded = false; // becomes true if we find a nextEmbedded action + const lexer: monarchCommon.ILexer = { + languageId: languageId, + includeLF: bool(json.includeLF, false), + noThrow: false, // raise exceptions during compilation + maxStack: 100, + start: (typeof json.start === 'string' ? json.start : null), + ignoreCase: bool(json.ignoreCase, false), + unicode: bool(json.unicode, false), + tokenPostfix: string(json.tokenPostfix, '.' + languageId), + defaultToken: string(json.defaultToken, 'source'), + usesEmbedded: false, // becomes true if we find a nextEmbedded action + stateNames: {}, + tokenizer: {}, + brackets: [] + }; // For calling compileAction later on const lexerMin: monarchCommon.ILexerMin = json; diff --git a/src/vs/editor/standalone/test/browser/monarch.test.ts b/src/vs/editor/standalone/test/browser/monarch.test.ts index 12feefa27d5..fbe58b62169 100644 --- a/src/vs/editor/standalone/test/browser/monarch.test.ts +++ b/src/vs/editor/standalone/test/browser/monarch.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Token, TokenizationRegistry } from 'vs/editor/common/languages'; diff --git a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts index 92e385564ac..745c0075541 100644 --- a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Color } from 'vs/base/common/color'; import { Emitter } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/standalone/test/browser/standaloneServices.test.ts b/src/vs/editor/standalone/test/browser/standaloneServices.test.ts index e44de083b98..649783848e3 100644 --- a/src/vs/editor/standalone/test/browser/standaloneServices.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneServices.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/browser/commands/shiftCommand.test.ts b/src/vs/editor/test/browser/commands/shiftCommand.test.ts index a769b21c4f9..8d98e803658 100644 --- a/src/vs/editor/test/browser/commands/shiftCommand.test.ts +++ b/src/vs/editor/test/browser/commands/shiftCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; diff --git a/src/vs/editor/test/browser/commands/sideEditing.test.ts b/src/vs/editor/test/browser/commands/sideEditing.test.ts index 6cdb2657aa7..fbd9a36c95a 100644 --- a/src/vs/editor/test/browser/commands/sideEditing.test.ts +++ b/src/vs/editor/test/browser/commands/sideEditing.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts index 5b4d0994a62..15f9bb999d4 100644 --- a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts +++ b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TrimTrailingWhitespaceCommand, trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; diff --git a/src/vs/editor/test/browser/config/editorConfiguration.test.ts b/src/vs/editor/test/browser/config/editorConfiguration.test.ts index 14053ade4a7..88333ac29a0 100644 --- a/src/vs/editor/test/browser/config/editorConfiguration.test.ts +++ b/src/vs/editor/test/browser/config/editorConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEnvConfiguration } from 'vs/editor/browser/config/editorConfiguration'; import { migrateOptions } from 'vs/editor/browser/config/migrateOptions'; diff --git a/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts b/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts index d44cc97a7f4..a2e3ace58f6 100644 --- a/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ComputedEditorOptions } from 'vs/editor/browser/config/editorConfiguration'; import { EditorLayoutInfo, EditorLayoutInfoComputer, EditorMinimapOptions, EditorOption, EditorOptions, InternalEditorRenderLineNumbersOptions, InternalEditorScrollbarOptions, RenderLineNumbersType, RenderMinimap } from 'vs/editor/common/config/editorOptions'; diff --git a/src/vs/editor/test/browser/controller/cursor.integrationTest.ts b/src/vs/editor/test/browser/controller/cursor.integrationTest.ts index 6373a6fd603..bd956a35398 100644 --- a/src/vs/editor/test/browser/controller/cursor.integrationTest.ts +++ b/src/vs/editor/test/browser/controller/cursor.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Selection } from 'vs/editor/common/core/selection'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index a86e95c1303..bb2abffb9c9 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts index 2e312c53e3e..c4bee4b9f2b 100644 --- a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts +++ b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreNavigationCommands } from 'vs/editor/browser/coreCommands'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/browser/controller/textAreaInput.test.ts b/src/vs/editor/test/browser/controller/textAreaInput.test.ts index 19fcf3b1970..fd2e23e3751 100644 --- a/src/vs/editor/test/browser/controller/textAreaInput.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaInput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { OperatingSystem } from 'vs/base/common/platform'; diff --git a/src/vs/editor/test/browser/controller/textAreaState.test.ts b/src/vs/editor/test/browser/controller/textAreaState.test.ts index 087e99311f2..2baa9ddda39 100644 --- a/src/vs/editor/test/browser/controller/textAreaState.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaState.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ITextAreaWrapper, PagedScreenReaderStrategy, TextAreaState } from 'vs/editor/browser/controller/textAreaState'; diff --git a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts index b8eb27e6d0f..9160c07070a 100644 --- a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts +++ b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/browser/services/openerService.test.ts b/src/vs/editor/test/browser/services/openerService.test.ts index 26dea91e681..d732776a800 100644 --- a/src/vs/editor/test/browser/services/openerService.test.ts +++ b/src/vs/editor/test/browser/services/openerService.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/browser/testCommand.ts b/src/vs/editor/test/browser/testCommand.ts index 60abb6d301f..e12d2cc526a 100644 --- a/src/vs/editor/test/browser/testCommand.ts +++ b/src/vs/editor/test/browser/testCommand.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IRange } from 'vs/editor/common/core/range'; import { Selection, ISelection } from 'vs/editor/common/core/selection'; import { ICommand, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; diff --git a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts index ab0d682a7be..4fcfa5cf3cd 100644 --- a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts +++ b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MinimapCharRendererFactory } from 'vs/editor/browser/viewParts/minimap/minimapCharRendererFactory'; import { Constants } from 'vs/editor/browser/viewParts/minimap/minimapCharSheet'; diff --git a/src/vs/editor/test/browser/view/viewLayer.test.ts b/src/vs/editor/test/browser/view/viewLayer.test.ts index 0fcb0b1d57d..0f9588d870c 100644 --- a/src/vs/editor/test/browser/view/viewLayer.test.ts +++ b/src/vs/editor/test/browser/view/viewLayer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILine, RenderedLinesCollection } from 'vs/editor/browser/view/viewLayer'; diff --git a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts index 0974ab8d183..292aa0ed1fa 100644 --- a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts +++ b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; diff --git a/src/vs/editor/test/browser/viewModel/testViewModel.ts b/src/vs/editor/test/browser/viewModel/testViewModel.ts index fe7f0c6e2db..36749b71bd6 100644 --- a/src/vs/editor/test/browser/viewModel/testViewModel.ts +++ b/src/vs/editor/test/browser/viewModel/testViewModel.ts @@ -22,6 +22,8 @@ export function testViewModel(text: string[], options: IEditorOptions, callback: const viewModel = new ViewModel(EDITOR_ID, configuration, model, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!, testLanguageConfigurationService, new TestThemeService(), { setVisibleLines(visibleLines, stabilized) { }, + }, { + batchChanges: (cb) => cb(), }); callback(viewModel, model); diff --git a/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts b/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts index 02786c056b3..a89fc01e245 100644 --- a/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts +++ b/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts index 4b515f974b2..ff16b570be1 100644 --- a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts index 192b4b6d8e4..a09d5c98f18 100644 --- a/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts +++ b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/browser/widget/diffEditorWidget.test.ts b/src/vs/editor/test/browser/widget/diffEditorWidget.test.ts index 1f08e089c2e..53ecda34e13 100644 --- a/src/vs/editor/test/browser/widget/diffEditorWidget.test.ts +++ b/src/vs/editor/test/browser/widget/diffEditorWidget.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { UnchangedRegion } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; import { LineRange } from 'vs/editor/common/core/lineRange'; diff --git a/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts b/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts new file mode 100644 index 00000000000..5ae9f3d81f5 --- /dev/null +++ b/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts @@ -0,0 +1,226 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from "assert"; +import { DisposableStore } from "vs/base/common/lifecycle"; +import { IObservable, derivedHandleChanges } from "vs/base/common/observable"; +import { ensureNoDisposablesAreLeakedInTestSuite } from "vs/base/test/common/utils"; +import { ICodeEditor } from "vs/editor/browser/editorBrowser"; +import { ObservableCodeEditor, observableCodeEditor } from "vs/editor/browser/observableCodeEditor"; +import { Position } from "vs/editor/common/core/position"; +import { Range } from "vs/editor/common/core/range"; +import { ViewModel } from "vs/editor/common/viewModel/viewModelImpl"; +import { withTestCodeEditor } from "vs/editor/test/browser/testCodeEditor"; + +suite("CodeEditorWidget", () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + function withTestFixture( + cb: (args: { editor: ICodeEditor; viewModel: ViewModel; log: Log; derived: IObservable; }) => void + ) { + withEditorSetupTestFixture(undefined, cb); + } + + function withEditorSetupTestFixture( + preSetupCallback: + | ((editor: ICodeEditor, disposables: DisposableStore) => void) + | undefined, + cb: (args: { editor: ICodeEditor; viewModel: ViewModel; log: Log; derived: IObservable; }) => void + ) { + withTestCodeEditor("hello world", {}, (editor, viewModel) => { + const disposables = new DisposableStore(); + preSetupCallback?.(editor, disposables); + const obsEditor = observableCodeEditor(editor); + const log = new Log(); + + const derived = derivedHandleChanges( + { + createEmptyChangeSummary: () => undefined, + handleChange: (context) => { + const obsName = observableName(context.changedObservable, obsEditor); + log.log(`handle change: ${obsName} ${formatChange(context.change)}`); + return true; + }, + }, + (reader) => { + const versionId = obsEditor.versionId.read(reader); + const selection = obsEditor.selections.read(reader)?.map((s) => s.toString()).join(", "); + obsEditor.onDidType.read(reader); + + const str = `running derived: selection: ${selection}, value: ${versionId}`; + log.log(str); + return str; + } + ); + + derived.recomputeInitiallyAndOnChange(disposables); + assert.deepStrictEqual(log.getAndClearEntries(), [ + "running derived: selection: [1,1 -> 1,1], value: 1", + ]); + + cb({ editor, viewModel, log, derived }); + + disposables.dispose(); + }); + } + + test("setPosition", () => + withTestFixture(({ editor, log }) => { + editor.setPosition(new Position(1, 2)); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":1,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"api","reason":0}', + "running derived: selection: [1,2 -> 1,2], value: 1", + ]); + })); + + test("keyboard.type", () => + withTestFixture(({ editor, log }) => { + editor.trigger("keyboard", "type", { text: "abc" }); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'handle change: editor.onDidType "abc"', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', + 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', + 'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', + "running derived: selection: [1,4 -> 1,4], value: 4", + ]); + })); + + test("keyboard.type and set position", () => + withTestFixture(({ editor, log }) => { + editor.trigger("keyboard", "type", { text: "abc" }); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'handle change: editor.onDidType "abc"', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', + 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', + 'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', + "running derived: selection: [1,4 -> 1,4], value: 4", + ]); + + editor.setPosition(new Position(1, 5), "test"); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'handle change: editor.selections {"selection":"[1,5 -> 1,5]","modelVersionId":4,"oldSelections":["[1,4 -> 1,4]"],"oldModelVersionId":4,"source":"test","reason":0}', + "running derived: selection: [1,5 -> 1,5], value: 4", + ]); + })); + + test("listener interaction (unforced)", () => { + let derived: IObservable; + let log: Log; + withEditorSetupTestFixture( + (editor, disposables) => { + disposables.add( + editor.onDidChangeModelContent(() => { + log.log(">>> before get"); + derived.get(); + log.log("<<< after get"); + }) + ); + }, + (args) => { + const editor = args.editor; + derived = args.derived; + log = args.log; + + editor.trigger("keyboard", "type", { text: "a" }); + assert.deepStrictEqual(log.getAndClearEntries(), [ + ">>> before get", + "<<< after get", + 'handle change: editor.onDidType "a"', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":2,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', + "running derived: selection: [1,2 -> 1,2], value: 2", + ]); + } + ); + }); + + test("listener interaction ()", () => { + let derived: IObservable; + let log: Log; + withEditorSetupTestFixture( + (editor, disposables) => { + disposables.add( + editor.onDidChangeModelContent(() => { + log.log(">>> before forceUpdate"); + observableCodeEditor(editor).forceUpdate(); + + log.log(">>> before get"); + derived.get(); + log.log("<<< after get"); + }) + ); + }, + (args) => { + const editor = args.editor; + derived = args.derived; + log = args.log; + + editor.trigger("keyboard", "type", { text: "a" }); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + ">>> before forceUpdate", + ">>> before get", + "handle change: editor.versionId undefined", + "running derived: selection: [1,2 -> 1,2], value: 2", + "<<< after get", + 'handle change: editor.onDidType "a"', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":2,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', + "running derived: selection: [1,2 -> 1,2], value: 2", + ]); + } + ); + }); +}); + +class Log { + private readonly entries: string[] = []; + public log(message: string): void { + this.entries.push(message); + } + + public getAndClearEntries(): string[] { + const entries = [...this.entries]; + this.entries.length = 0; + return entries; + } +} + +function formatChange(change: unknown) { + return JSON.stringify( + change, + (key, value) => { + if (value instanceof Range) { + return value.toString(); + } + if ( + value === false || + (Array.isArray(value) && value.length === 0) + ) { + return undefined; + } + return value; + } + ); +} + +function observableName(obs: IObservable, obsEditor: ObservableCodeEditor): string { + switch (obs) { + case obsEditor.selections: + return "editor.selections"; + case obsEditor.versionId: + return "editor.versionId"; + case obsEditor.onDidType: + return "editor.onDidType"; + default: + return "unknown"; + } +} diff --git a/src/vs/editor/test/common/controller/cursorAtomicMoveOperations.test.ts b/src/vs/editor/test/common/controller/cursorAtomicMoveOperations.test.ts index 200cb5e226a..9c2efda4f77 100644 --- a/src/vs/editor/test/common/controller/cursorAtomicMoveOperations.test.ts +++ b/src/vs/editor/test/common/controller/cursorAtomicMoveOperations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AtomicTabMoveOperations, Direction } from 'vs/editor/common/cursor/cursorAtomicMoveOperations'; diff --git a/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts b/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts index c2d8dc85a45..e90bdd5a571 100644 --- a/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts +++ b/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CursorColumns } from 'vs/editor/common/core/cursorColumns'; diff --git a/src/vs/editor/test/common/core/characterClassifier.test.ts b/src/vs/editor/test/common/core/characterClassifier.test.ts index dd4c4b02df3..4271d91f923 100644 --- a/src/vs/editor/test/common/core/characterClassifier.test.ts +++ b/src/vs/editor/test/common/core/characterClassifier.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier'; diff --git a/src/vs/editor/test/common/core/lineRange.test.ts b/src/vs/editor/test/common/core/lineRange.test.ts index b67b9bdfb7b..1b45b3f2829 100644 --- a/src/vs/editor/test/common/core/lineRange.test.ts +++ b/src/vs/editor/test/common/core/lineRange.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LineRange, LineRangeSet } from 'vs/editor/common/core/lineRange'; diff --git a/src/vs/editor/test/common/core/lineTokens.test.ts b/src/vs/editor/test/common/core/lineTokens.test.ts index 177e66774df..d2457fa2bc8 100644 --- a/src/vs/editor/test/common/core/lineTokens.test.ts +++ b/src/vs/editor/test/common/core/lineTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; import { LanguageIdCodec } from 'vs/editor/common/services/languagesRegistry'; diff --git a/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts b/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts index 39aead0a848..1811a08e90f 100644 --- a/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts +++ b/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { PositionOffsetTransformer } from 'vs/editor/common/core/positionToOffset'; diff --git a/src/vs/editor/test/common/core/range.test.ts b/src/vs/editor/test/common/core/range.test.ts index bf574592d4e..fcbb0cd0fcc 100644 --- a/src/vs/editor/test/common/core/range.test.ts +++ b/src/vs/editor/test/common/core/range.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/core/stringBuilder.test.ts b/src/vs/editor/test/common/core/stringBuilder.test.ts index af90a1f5eb1..6afe99db33a 100644 --- a/src/vs/editor/test/common/core/stringBuilder.test.ts +++ b/src/vs/editor/test/common/core/stringBuilder.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { writeUInt16LE } from 'vs/base/common/buffer'; import { CharCode } from 'vs/base/common/charCode'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/common/core/textEdit.test.ts b/src/vs/editor/test/common/core/textEdit.test.ts index f02e8a9bd50..4458eaf8a5b 100644 --- a/src/vs/editor/test/common/core/textEdit.test.ts +++ b/src/vs/editor/test/common/core/textEdit.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { StringText } from 'vs/editor/common/core/textEdit'; diff --git a/src/vs/editor/test/common/diff/diffComputer.test.ts b/src/vs/editor/test/common/diff/diffComputer.test.ts index 38378ef3826..651dc5a79f2 100644 --- a/src/vs/editor/test/common/diff/diffComputer.test.ts +++ b/src/vs/editor/test/common/diff/diffComputer.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Constants } from 'vs/base/common/uint'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts index 264292f82ff..514556238b5 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { splitLines } from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts index 2a21907778d..1c2ea8c3b49 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts index 9155b32a9ca..a1def431d2b 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts index 42d0eae8f81..6fced6cef3e 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AstNode, AstNodeKind, ListAstNode, TextAstNode } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast'; import { concat23Trees } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/concat23Trees'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts index 09c9e34a124..87215503dc4 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore, disposeOnReturn } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts index eb49c1048d1..57086d81ff8 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Length, lengthAdd, lengthDiffNonNegative, lengthToObj, toLength } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts index 45d4dcc6833..2b5026e4f0c 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DenseKeyProvider, SmallImmutableSet } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/smallImmutableSet'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts index a83b30a66eb..f306d99ccab 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguageId, MetadataConsts, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; diff --git a/src/vs/editor/test/common/model/editStack.test.ts b/src/vs/editor/test/common/model/editStack.test.ts index d220d12e668..da409c9d8a3 100644 --- a/src/vs/editor/test/common/model/editStack.test.ts +++ b/src/vs/editor/test/common/model/editStack.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Selection } from 'vs/editor/common/core/selection'; import { TextChange } from 'vs/editor/common/core/textChange'; diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index aa39ad03e8c..d2af9519432 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts index 06803315a7b..2228a6174a4 100644 --- a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts +++ b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { EndOfLinePreference, EndOfLineSequence } from 'vs/editor/common/model'; diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index 06534b6e2a6..dd2fd332be5 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; import { IntervalNode, IntervalTree, NodeColor, SENTINEL, getNodeColor, intervalCompare, nodeAcceptEdit, setNodeStickiness } from 'vs/editor/common/model/intervalTree'; @@ -912,4 +912,3 @@ function assertValidTree(T: IntervalTree): void { } //#endregion - diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts index 77bc1e74cc1..a4604874d65 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { DefaultEndOfLine } from 'vs/editor/common/model'; diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts index 22f182090b8..662ef1fe8fd 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as strings from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DefaultEndOfLine } from 'vs/editor/common/model'; diff --git a/src/vs/editor/test/common/model/model.line.test.ts b/src/vs/editor/test/common/model/model.line.test.ts index 5eb7462e045..91279c1d70d 100644 --- a/src/vs/editor/test/common/model/model.line.test.ts +++ b/src/vs/editor/test/common/model/model.line.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; diff --git a/src/vs/editor/test/common/model/model.modes.test.ts b/src/vs/editor/test/common/model/model.modes.test.ts index fa48350404f..66df53d83df 100644 --- a/src/vs/editor/test/common/model/model.modes.test.ts +++ b/src/vs/editor/test/common/model/model.modes.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index a75e86e7b59..e699174a650 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index dda14417b1a..c00d0ce8f2a 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/common/model/modelEditOperation.test.ts b/src/vs/editor/test/common/model/modelEditOperation.test.ts index a2cd24ce4f4..0aeebe90c6f 100644 --- a/src/vs/editor/test/common/model/modelEditOperation.test.ts +++ b/src/vs/editor/test/common/model/modelEditOperation.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/modelInjectedText.test.ts b/src/vs/editor/test/common/model/modelInjectedText.test.ts index 72d0f9ba0a3..f7c023c4f8a 100644 --- a/src/vs/editor/test/common/model/modelInjectedText.test.ts +++ b/src/vs/editor/test/common/model/modelInjectedText.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index 007033dc790..01cc7cb6ef6 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { WordCharacterClassifier } from 'vs/editor/common/core/wordCharacterClassifier'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -1817,6 +1817,22 @@ suite('buffer api', () => { assert.strictEqual(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); assert.strictEqual(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); }); + + test('getNearestChunk', () => { + const pieceTree = createTextBuffer(['012345678']); + ds.add(pieceTree); + const pt = pieceTree.getPieceTree(); + + pt.insert(3, 'ABC'); + assert.equal(pt.getLineContent(1), '012ABC345678'); + assert.equal(pt.getNearestChunk(3), 'ABC'); + assert.equal(pt.getNearestChunk(6), '345678'); + + pt.delete(9, 1); + assert.equal(pt.getLineContent(1), '012ABC34578'); + assert.equal(pt.getNearestChunk(6), '345'); + assert.equal(pt.getNearestChunk(9), '78'); + }); }); suite('search offset cache', () => { diff --git a/src/vs/editor/test/common/model/textChange.test.ts b/src/vs/editor/test/common/model/textChange.test.ts index 164e7ff92e1..a58430b309f 100644 --- a/src/vs/editor/test/common/model/textChange.test.ts +++ b/src/vs/editor/test/common/model/textChange.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { compressConsecutiveTextChanges, TextChange } from 'vs/editor/common/core/textChange'; diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index dc6037d6df4..3270a563386 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { UTF8_BOM_CHARACTER } from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index 91ec41810f3..0f03a1e0730 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/textModelTokens.test.ts b/src/vs/editor/test/common/model/textModelTokens.test.ts index b425cc30fbf..34171ea9b1d 100644 --- a/src/vs/editor/test/common/model/textModelTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { RangePriorityQueueImpl } from 'vs/editor/common/model/textModelTokens'; diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index cdb8528819b..54ef6b8d628 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/tokensStore.test.ts b/src/vs/editor/test/common/model/tokensStore.test.ts index 7ceba9ef598..f4e9413a422 100644 --- a/src/vs/editor/test/common/model/tokensStore.test.ts +++ b/src/vs/editor/test/common/model/tokensStore.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/test/common/modes/languageConfiguration.test.ts b/src/vs/editor/test/common/modes/languageConfiguration.test.ts index 97c74722cc9..28e6340d9f3 100644 --- a/src/vs/editor/test/common/modes/languageConfiguration.test.ts +++ b/src/vs/editor/test/common/modes/languageConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; diff --git a/src/vs/editor/test/common/modes/languageSelector.test.ts b/src/vs/editor/test/common/modes/languageSelector.test.ts index ce3aa3f4078..3de1b762b59 100644 --- a/src/vs/editor/test/common/modes/languageSelector.test.ts +++ b/src/vs/editor/test/common/modes/languageSelector.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguageSelector, score } from 'vs/editor/common/languageSelector'; diff --git a/src/vs/editor/test/common/modes/linkComputer.test.ts b/src/vs/editor/test/common/modes/linkComputer.test.ts index 49411db88e9..2d769837672 100644 --- a/src/vs/editor/test/common/modes/linkComputer.test.ts +++ b/src/vs/editor/test/common/modes/linkComputer.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILink } from 'vs/editor/common/languages'; import { ILinkComputerTarget, computeLinks } from 'vs/editor/common/languages/linkComputer'; diff --git a/src/vs/editor/test/common/modes/supports/autoClosingPairsRules.ts b/src/vs/editor/test/common/modes/supports/autoClosingPairsRules.ts index 968bd350892..0f5ebc499bd 100644 --- a/src/vs/editor/test/common/modes/supports/autoClosingPairsRules.ts +++ b/src/vs/editor/test/common/modes/supports/autoClosingPairsRules.ts @@ -3,7 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAutoClosingPair } from 'vs/editor/common/languages/languageConfiguration'; +import { IAutoClosingPair, IAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; + +export const javascriptAutoClosingPairsRules: IAutoClosingPairConditional[] = [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '\'', close: '\'', notIn: ['string', 'comment'] }, + { open: '"', close: '"', notIn: ['string'] }, + { open: '`', close: '`', notIn: ['string', 'comment'] }, + { open: '/**', close: ' */', notIn: ['string'] } +]; export const latexAutoClosingPairsRules: IAutoClosingPair[] = [ { open: '\\left(', close: '\\right)' }, diff --git a/src/vs/editor/test/common/modes/supports/characterPair.test.ts b/src/vs/editor/test/common/modes/supports/characterPair.test.ts index 70c763ec16e..e92b7db2e6e 100644 --- a/src/vs/editor/test/common/modes/supports/characterPair.test.ts +++ b/src/vs/editor/test/common/modes/supports/characterPair.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; diff --git a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts index 90d89185aa4..20170cb8f48 100644 --- a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts +++ b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { BracketElectricCharacterSupport, IElectricAction } from 'vs/editor/common/languages/supports/electricCharacter'; diff --git a/src/vs/editor/test/common/modes/supports/onEnter.test.ts b/src/vs/editor/test/common/modes/supports/onEnter.test.ts index 1daa14e1607..4ed40eb1abb 100644 --- a/src/vs/editor/test/common/modes/supports/onEnter.test.ts +++ b/src/vs/editor/test/common/modes/supports/onEnter.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharacterPair, IndentAction } from 'vs/editor/common/languages/languageConfiguration'; import { OnEnterSupport } from 'vs/editor/common/languages/supports/onEnter'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/onEnterRules'; diff --git a/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts b/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts index f58f7105d7e..5fd90a71561 100644 --- a/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts +++ b/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { BracketsUtils } from 'vs/editor/common/languages/supports/richEditBrackets'; diff --git a/src/vs/editor/test/common/modes/supports/tokenization.test.ts b/src/vs/editor/test/common/modes/supports/tokenization.test.ts index 26854898f03..b5386ec1b3b 100644 --- a/src/vs/editor/test/common/modes/supports/tokenization.test.ts +++ b/src/vs/editor/test/common/modes/supports/tokenization.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { FontStyle } from 'vs/editor/common/encodedTokenAttributes'; import { ColorMap, ExternalThemeTrieElement, ParsedTokenThemeRule, ThemeTrieElementRule, TokenTheme, parseTokenTheme, strcmp } from 'vs/editor/common/languages/supports/tokenization'; diff --git a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts index 7593ea14706..af7015a19b6 100644 --- a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts +++ b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ColorId, FontStyle, MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; diff --git a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts index 5dc2dd11f16..781ff450211 100644 --- a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts +++ b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/services/languageService.test.ts b/src/vs/editor/test/common/services/languageService.test.ts index b0b54a96746..e8317e6ae88 100644 --- a/src/vs/editor/test/common/services/languageService.test.ts +++ b/src/vs/editor/test/common/services/languageService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { LanguageService } from 'vs/editor/common/services/languageService'; diff --git a/src/vs/editor/test/common/services/languagesAssociations.test.ts b/src/vs/editor/test/common/services/languagesAssociations.test.ts index 689457fe678..7d6ce3ba4dc 100644 --- a/src/vs/editor/test/common/services/languagesAssociations.test.ts +++ b/src/vs/editor/test/common/services/languagesAssociations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getMimeTypes, registerPlatformLanguageAssociation, registerConfiguredLanguageAssociation } from 'vs/editor/common/services/languagesAssociations'; diff --git a/src/vs/editor/test/common/services/languagesRegistry.test.ts b/src/vs/editor/test/common/services/languagesRegistry.test.ts index 74ec1559d43..d4715b8534c 100644 --- a/src/vs/editor/test/common/services/languagesRegistry.test.ts +++ b/src/vs/editor/test/common/services/languagesRegistry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguagesRegistry } from 'vs/editor/common/services/languagesRegistry'; diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index 53eb701ecfb..cd4b53d86df 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/test/common/services/semanticTokensDto.test.ts b/src/vs/editor/test/common/services/semanticTokensDto.test.ts index b32e7e66c74..7093691179d 100644 --- a/src/vs/editor/test/common/services/semanticTokensDto.test.ts +++ b/src/vs/editor/test/common/services/semanticTokensDto.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IFullSemanticTokensDto, IDeltaSemanticTokensDto, encodeSemanticTokensDto, ISemanticTokensDto, decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; import { VSBuffer } from 'vs/base/common/buffer'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts b/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts index bfec2c00b1a..1128768e919 100644 --- a/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts +++ b/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; diff --git a/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts b/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts index 11410512dc6..c7fbd1afff2 100644 --- a/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts +++ b/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IModelService } from 'vs/editor/common/services/model'; diff --git a/src/vs/editor/test/common/services/unicodeTextModelHighlighter.test.ts b/src/vs/editor/test/common/services/unicodeTextModelHighlighter.test.ts index b646a4c18be..9b5351bd618 100644 --- a/src/vs/editor/test/common/services/unicodeTextModelHighlighter.test.ts +++ b/src/vs/editor/test/common/services/unicodeTextModelHighlighter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { UnicodeHighlighterOptions, UnicodeTextModelHighlighter } from 'vs/editor/common/services/unicodeTextModelHighlighter'; diff --git a/src/vs/editor/test/common/view/overviewZoneManager.test.ts b/src/vs/editor/test/common/view/overviewZoneManager.test.ts index 5896b2a928b..b488141d7d7 100644 --- a/src/vs/editor/test/common/view/overviewZoneManager.test.ts +++ b/src/vs/editor/test/common/view/overviewZoneManager.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ColorZone, OverviewRulerZone, OverviewZoneManager } from 'vs/editor/common/viewModel/overviewZoneManager'; diff --git a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts index ecfde1e3d91..d1058c42293 100644 --- a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { DecorationSegment, LineDecoration, LineDecorationsNormalizer } from 'vs/editor/common/viewLayout/lineDecorations'; diff --git a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts index c3f0fa635ab..58f9217ea56 100644 --- a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts +++ b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorWhitespace, LinesLayout } from 'vs/editor/common/viewLayout/linesLayout'; diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 2fdbeaefdf8..22e1c60f7af 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import * as strings from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/common/viewModel/glyphLanesModel.test.ts b/src/vs/editor/test/common/viewModel/glyphLanesModel.test.ts index 7c0522a84f0..84659a045c6 100644 --- a/src/vs/editor/test/common/viewModel/glyphLanesModel.test.ts +++ b/src/vs/editor/test/common/viewModel/glyphLanesModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { GlyphMarginLanesModel, } from 'vs/editor/common/viewModel/glyphLanesModel'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts index 85792a42390..b771b7e5ff8 100644 --- a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts +++ b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { PositionAffinity } from 'vs/editor/common/model'; import { ModelDecorationInjectedTextOptions } from 'vs/editor/common/model/textModel'; diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 37727d4c8a1..a1defd0c4f5 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorOptions, WrappingIndent } from 'vs/editor/common/config/editorOptions'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; diff --git a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts index 1fdb7a2c10b..e1820e6e920 100644 --- a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { toUint32 } from 'vs/base/common/uint'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { PrefixSumComputer, PrefixSumIndexOfResult } from 'vs/editor/common/model/prefixSumComputer'; diff --git a/src/vs/editor/test/node/classification/typescript.test.ts b/src/vs/editor/test/node/classification/typescript.test.ts index e9e8d3fec1a..d2657a7ce41 100644 --- a/src/vs/editor/test/node/classification/typescript.test.ts +++ b/src/vs/editor/test/node/classification/typescript.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import * as fs from 'fs'; // import { getPathFromAmdModule } from 'vs/base/test/node/testUtils'; diff --git a/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts index 995472ca78f..57a34dfc364 100644 --- a/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts +++ b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; import { RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; diff --git a/src/vs/editor/test/node/diffing/fixtures.test.ts b/src/vs/editor/test/node/diffing/fixtures.test.ts index e944d133bef..901ed758ff1 100644 --- a/src/vs/editor/test/node/diffing/fixtures.test.ts +++ b/src/vs/editor/test/node/diffing/fixtures.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs'; import { join, resolve } from 'path'; import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; @@ -123,7 +123,7 @@ suite('diffing fixtures', () => { } test(`test`, () => { - runTest('shifting-twice', 'advanced'); + runTest('issue-214049', 'advanced'); }); for (const folder of folders) { diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-214049/1.txt b/src/vs/editor/test/node/diffing/fixtures/issue-214049/1.txt new file mode 100644 index 00000000000..db510b75635 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-214049/1.txt @@ -0,0 +1,2 @@ +hello world; +y \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-214049/2.txt b/src/vs/editor/test/node/diffing/fixtures/issue-214049/2.txt new file mode 100644 index 00000000000..0dc735e1c5a --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-214049/2.txt @@ -0,0 +1,3 @@ +hello world; +// new line +y \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-214049/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/issue-214049/advanced.expected.diff.json new file mode 100644 index 00000000000..181c78999fa --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-214049/advanced.expected.diff.json @@ -0,0 +1,26 @@ +{ + "diffs": [ + { + "innerChanges": [ + { + "modifiedRange": "[1,13 -> 1,13 EOL]", + "originalRange": "[1,13 -> 1,14 EOL]" + }, + { + "modifiedRange": "[2,1 -> 3,1]", + "originalRange": "[2,1 -> 2,1]" + } + ], + "modifiedRange": "[1,3)", + "originalRange": "[1,2)" + } + ], + "modified": { + "content": "hello world;\n// new line\ny", + "fileName": "./2.txt" + }, + "original": { + "content": "hello world; \ny", + "fileName": "./1.txt" + } +} diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-214049/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/issue-214049/legacy.expected.diff.json new file mode 100644 index 00000000000..727c2e8eb55 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-214049/legacy.expected.diff.json @@ -0,0 +1,17 @@ +{ + "original": { + "content": "hello world; \ny", + "fileName": "./1.txt" + }, + "modified": { + "content": "hello world;\n// new line\ny", + "fileName": "./2.txt" + }, + "diffs": [ + { + "originalRange": "[1,2)", + "modifiedRange": "[1,3)", + "innerChanges": null + } + ] +} \ No newline at end of file diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 8ae213d9ce8..880b9017c9b 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3745,6 +3745,11 @@ declare namespace monaco.editor { * Defaults to false. */ peekWidgetDefaultFocus?: 'tree' | 'editor'; + /** + * Sets a placeholder for the editor. + * If set, the placeholder is shown if the editor is empty. + */ + placeholder?: string | undefined; /** * Controls whether the definition link opens element in the peek widget. * Defaults to false. @@ -4939,70 +4944,71 @@ declare namespace monaco.editor { pasteAs = 85, parameterHints = 86, peekWidgetDefaultFocus = 87, - definitionLinkOpensInPeek = 88, - quickSuggestions = 89, - quickSuggestionsDelay = 90, - readOnly = 91, - readOnlyMessage = 92, - renameOnType = 93, - renderControlCharacters = 94, - renderFinalNewline = 95, - renderLineHighlight = 96, - renderLineHighlightOnlyWhenFocus = 97, - renderValidationDecorations = 98, - renderWhitespace = 99, - revealHorizontalRightPadding = 100, - roundedSelection = 101, - rulers = 102, - scrollbar = 103, - scrollBeyondLastColumn = 104, - scrollBeyondLastLine = 105, - scrollPredominantAxis = 106, - selectionClipboard = 107, - selectionHighlight = 108, - selectOnLineNumbers = 109, - showFoldingControls = 110, - showUnused = 111, - snippetSuggestions = 112, - smartSelect = 113, - smoothScrolling = 114, - stickyScroll = 115, - stickyTabStops = 116, - stopRenderingLineAfter = 117, - suggest = 118, - suggestFontSize = 119, - suggestLineHeight = 120, - suggestOnTriggerCharacters = 121, - suggestSelection = 122, - tabCompletion = 123, - tabIndex = 124, - unicodeHighlighting = 125, - unusualLineTerminators = 126, - useShadowDOM = 127, - useTabStops = 128, - wordBreak = 129, - wordSegmenterLocales = 130, - wordSeparators = 131, - wordWrap = 132, - wordWrapBreakAfterCharacters = 133, - wordWrapBreakBeforeCharacters = 134, - wordWrapColumn = 135, - wordWrapOverride1 = 136, - wordWrapOverride2 = 137, - wrappingIndent = 138, - wrappingStrategy = 139, - showDeprecated = 140, - inlayHints = 141, - editorClassName = 142, - pixelRatio = 143, - tabFocusMode = 144, - layoutInfo = 145, - wrappingInfo = 146, - defaultColorDecorators = 147, - colorDecoratorsActivatedOn = 148, - inlineCompletionsAccessibilityVerbose = 149, - quickSuggestionsMinimumLength = 150, - tabSuggest = 151 + placeholder = 88, + definitionLinkOpensInPeek = 89, + quickSuggestions = 90, + quickSuggestionsDelay = 91, + readOnly = 92, + readOnlyMessage = 93, + renameOnType = 94, + renderControlCharacters = 95, + renderFinalNewline = 96, + renderLineHighlight = 97, + renderLineHighlightOnlyWhenFocus = 98, + renderValidationDecorations = 99, + renderWhitespace = 100, + revealHorizontalRightPadding = 101, + roundedSelection = 102, + rulers = 103, + scrollbar = 104, + scrollBeyondLastColumn = 105, + scrollBeyondLastLine = 106, + scrollPredominantAxis = 107, + selectionClipboard = 108, + selectionHighlight = 109, + selectOnLineNumbers = 110, + showFoldingControls = 111, + showUnused = 112, + snippetSuggestions = 113, + smartSelect = 114, + smoothScrolling = 115, + stickyScroll = 116, + stickyTabStops = 117, + stopRenderingLineAfter = 118, + suggest = 119, + suggestFontSize = 120, + suggestLineHeight = 121, + suggestOnTriggerCharacters = 122, + suggestSelection = 123, + tabCompletion = 124, + tabIndex = 125, + unicodeHighlighting = 126, + unusualLineTerminators = 127, + useShadowDOM = 128, + useTabStops = 129, + wordBreak = 130, + wordSegmenterLocales = 131, + wordSeparators = 132, + wordWrap = 133, + wordWrapBreakAfterCharacters = 134, + wordWrapBreakBeforeCharacters = 135, + wordWrapColumn = 136, + wordWrapOverride1 = 137, + wordWrapOverride2 = 138, + wrappingIndent = 139, + wrappingStrategy = 140, + showDeprecated = 141, + inlayHints = 142, + editorClassName = 143, + pixelRatio = 144, + tabFocusMode = 145, + layoutInfo = 146, + wrappingInfo = 147, + defaultColorDecorators = 148, + colorDecoratorsActivatedOn = 149, + inlineCompletionsAccessibilityVerbose = 150, + quickSuggestionsMinimumLength = 151, + tabSuggest = 152 } export const EditorOptions: { @@ -5015,8 +5021,8 @@ declare namespace monaco.editor { screenReaderAnnounceInlineSuggestion: IEditorOption; autoClosingBrackets: IEditorOption; autoClosingComments: IEditorOption; - autoClosingDelete: IEditorOption; - autoClosingOvertype: IEditorOption; + autoClosingDelete: IEditorOption; + autoClosingOvertype: IEditorOption; autoClosingQuotes: IEditorOption; autoIndent: IEditorOption; automaticLayout: IEditorOption; @@ -5028,7 +5034,7 @@ declare namespace monaco.editor { codeLensFontFamily: IEditorOption; codeLensFontSize: IEditorOption; colorDecorators: IEditorOption; - colorDecoratorActivatedOn: IEditorOption; + colorDecoratorActivatedOn: IEditorOption; colorDecoratorsLimit: IEditorOption; columnSelection: IEditorOption; comments: IEditorOption>>; @@ -5095,6 +5101,7 @@ declare namespace monaco.editor { pasteAs: IEditorOption>>; parameterHints: IEditorOption>>; peekWidgetDefaultFocus: IEditorOption; + placeholder: IEditorOption; definitionLinkOpensInPeek: IEditorOption; quickSuggestions: IEditorOption; quickSuggestionsDelay: IEditorOption; @@ -5138,13 +5145,13 @@ declare namespace monaco.editor { tabSuggest: IEditorOption; tabIndex: IEditorOption; unicodeHighlight: IEditorOption; - unusualLineTerminators: IEditorOption; + unusualLineTerminators: IEditorOption; useShadowDOM: IEditorOption; useTabStops: IEditorOption; wordBreak: IEditorOption; wordSegmenterLocales: IEditorOption; wordSeparators: IEditorOption; - wordWrap: IEditorOption; + wordWrap: IEditorOption; wordWrapBreakAfterCharacters: IEditorOption; wordWrapBreakBeforeCharacters: IEditorOption; wordWrapColumn: IEditorOption; @@ -7981,6 +7988,11 @@ declare namespace monaco.languages { arguments?: any[]; } + export interface CommentThreadRevealOptions { + preserveFocus: boolean; + focusReply: boolean; + } + export interface CommentAuthorInformation { name: string; iconPath?: UriComponents; @@ -8095,7 +8107,7 @@ declare namespace monaco.languages { * * @param document The document to provide mapped edits for. * @param codeBlocks Code blocks that come from an LLM's reply. - * "Insert at cursor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. + * "Apply in Editor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. * @param context The context for providing mapped edits. * @param token A cancellation token. * @returns A provider result of text edits. diff --git a/src/vs/platform/accessibility/browser/accessibilityService.ts b/src/vs/platform/accessibility/browser/accessibilityService.ts index bd84abbc6dc..408fbc07b2a 100644 --- a/src/vs/platform/accessibility/browser/accessibilityService.ts +++ b/src/vs/platform/accessibility/browser/accessibilityService.ts @@ -24,6 +24,9 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe protected _systemMotionReduced: boolean; protected readonly _onDidChangeReducedMotion = new Emitter(); + private _linkUnderlinesEnabled: boolean; + protected readonly _onDidChangeLinkUnderline = new Emitter(); + constructor( @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ILayoutService private readonly _layoutService: ILayoutService, @@ -50,7 +53,10 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe this._systemMotionReduced = reduceMotionMatcher.matches; this._configMotionReduced = this._configurationService.getValue<'auto' | 'on' | 'off'>('workbench.reduceMotion'); + this._linkUnderlinesEnabled = this._configurationService.getValue('accessibility.underlineLinks'); + this.initReducedMotionListeners(reduceMotionMatcher); + this.initLinkUnderlineListeners(); } private initReducedMotionListeners(reduceMotionMatcher: MediaQueryList) { @@ -72,6 +78,29 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe this._register(this.onDidChangeReducedMotion(() => updateRootClasses())); } + private initLinkUnderlineListeners() { + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('accessibility.underlineLinks')) { + const linkUnderlinesEnabled = this._configurationService.getValue('accessibility.underlineLinks'); + this._linkUnderlinesEnabled = linkUnderlinesEnabled; + this._onDidChangeLinkUnderline.fire(); + } + })); + + const updateLinkUnderlineClasses = () => { + const underlineLinks = this._linkUnderlinesEnabled; + this._layoutService.mainContainer.classList.toggle('underline-links', underlineLinks); + }; + + updateLinkUnderlineClasses(); + + this._register(this.onDidChangeLinkUnderlines(() => updateLinkUnderlineClasses())); + } + + public onDidChangeLinkUnderlines(listener: () => void) { + return this._onDidChangeLinkUnderline.event(listener); + } + get onDidChangeScreenReaderOptimized(): Event { return this._onDidChangeScreenReaderOptimized.event; } diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 65515c237ad..51d518e00f8 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -26,7 +26,7 @@ export interface IAccessibilitySignalService { playSignalLoop(signal: AccessibilitySignal, milliseconds: number): IDisposable; getEnabledState(signal: AccessibilitySignal, userGesture: boolean, modality?: AccessibilityModality | undefined): IValueWithChangeEvent; - getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality): number; + getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality, mode: 'line' | 'positional'): number; /** * Avoid this method and prefer `.playSignal`! * Only use it when you want to play the sound regardless of enablement, e.g. in the settings quick pick. @@ -67,7 +67,7 @@ export interface IAccessbilitySignalOptions { export class AccessibilitySignalService extends Disposable implements IAccessibilitySignalService { readonly _serviceBrand: undefined; private readonly sounds: Map = new Map(); - private readonly screenReaderAttached = observableFromEvent( + private readonly screenReaderAttached = observableFromEvent(this, this.accessibilityService.onDidChangeScreenReaderOptimized, () => /** @description accessibilityService.onDidChangeScreenReaderOptimized */ this.accessibilityService.isScreenReaderOptimized() ); @@ -241,10 +241,19 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi return this.getEnabledState(signal, false).onDidChange; } - public getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality): number { - const delaySettingsKey = signal.delaySettingsKey ?? 'accessibility.signalOptions.delays.general'; - const delaySettingsValue: { sound: number; announcement: number } = this.configurationService.getValue(delaySettingsKey); - return modality === 'sound' ? delaySettingsValue.sound : delaySettingsValue.announcement; + public getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality, mode: 'line' | 'positional'): number { + if (!this.configurationService.getValue('accessibility.signalOptions.debouncePositionChanges')) { + return 0; + } + let value: { sound: number; announcement: number }; + if (signal.name === AccessibilitySignal.errorAtPosition.name && mode === 'positional') { + value = this.configurationService.getValue('accessibility.signalOptions.experimental.delays.errorAtPosition'); + } else if (signal.name === AccessibilitySignal.warningAtPosition.name && mode === 'positional') { + value = this.configurationService.getValue('accessibility.signalOptions.experimental.delays.warningAtPosition'); + } else { + value = this.configurationService.getValue('accessibility.signalOptions.experimental.delays.general'); + } + return modality === 'sound' ? value.sound : value.announcement; } } diff --git a/src/vs/platform/actionWidget/browser/actionWidget.ts b/src/vs/platform/actionWidget/browser/actionWidget.ts index e7b26382eae..20a030b67ab 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.ts +++ b/src/vs/platform/actionWidget/browser/actionWidget.ts @@ -21,7 +21,7 @@ import { inputActiveOptionBackground, registerColor } from 'vs/platform/theme/co registerColor( 'actionBar.toggledBackground', - { dark: inputActiveOptionBackground, light: inputActiveOptionBackground, hcDark: inputActiveOptionBackground, hcLight: inputActiveOptionBackground, }, + inputActiveOptionBackground, localize('actionBar.toggledBackground', 'Background color for toggled action items in action bar.') ); diff --git a/src/vs/platform/actions/browser/buttonbar.ts b/src/vs/platform/actions/browser/buttonbar.ts index 165caec0bf1..530b1fe6170 100644 --- a/src/vs/platform/actions/browser/buttonbar.ts +++ b/src/vs/platform/actions/browser/buttonbar.ts @@ -6,11 +6,14 @@ import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button'; import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { ActionRunner, IAction, IActionRunner, SubmenuAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; -import { MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IToolBarRenderOptions } from 'vs/platform/actions/browser/toolbar'; +import { MenuId, IMenuService, MenuItemAction, IMenuActionOptions } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IHoverService } from 'vs/platform/hover/browser/hover'; @@ -66,7 +69,7 @@ export class WorkbenchButtonBar extends ButtonBar { super.dispose(); } - update(actions: IAction[]): void { + update(actions: IAction[], secondary: IAction[]): void { const conifgProvider: IButtonConfigProvider = this._options?.buttonConfigProvider ?? (() => ({ showLabel: true })); @@ -122,21 +125,51 @@ export class WorkbenchButtonBar extends ButtonBar { } else { tooltip = action.label; } - this._updateStore.add(this._hoverService.setupUpdatableHover(hoverDelegate, btn.element, tooltip)); + this._updateStore.add(this._hoverService.setupManagedHover(hoverDelegate, btn.element, tooltip)); this._updateStore.add(btn.onDidClick(async () => { this._actionRunner.run(action); })); } + + if (secondary.length > 0) { + + const btn = this.addButton({ + secondary: true, + ariaLabel: localize('moreActions', "More Actions") + }); + + btn.icon = Codicon.dropDownButton; + btn.element.classList.add('default-colors', 'monaco-text-button'); + + btn.enabled = true; + this._updateStore.add(this._hoverService.setupManagedHover(hoverDelegate, btn.element, localize('moreActions', "More Actions"))); + this._updateStore.add(btn.onDidClick(async () => { + this._contextMenuService.showContextMenu({ + getAnchor: () => btn.element, + getActions: () => secondary, + actionRunner: this._actionRunner, + onHide: () => btn.element.setAttribute('aria-expanded', 'false') + }); + btn.element.setAttribute('aria-expanded', 'true'); + + })); + } this._onDidChange.fire(this); } } +export interface IMenuWorkbenchButtonBarOptions extends IWorkbenchButtonBarOptions { + menuOptions?: IMenuActionOptions; + + toolbarOptions?: IToolBarRenderOptions; +} + export class MenuWorkbenchButtonBar extends WorkbenchButtonBar { constructor( container: HTMLElement, menuId: MenuId, - options: IWorkbenchButtonBarOptions | undefined, + options: IMenuWorkbenchButtonBarOptions | undefined, @IMenuService menuService: IMenuService, @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, @@ -153,12 +186,16 @@ export class MenuWorkbenchButtonBar extends WorkbenchButtonBar { this.clear(); - const actions = menu - .getActions({ renderShortTitle: true }) - .flatMap(entry => entry[1]); - - super.update(actions); + const primary: IAction[] = []; + const secondary: IAction[] = []; + createAndFillInActionBarActions( + menu, + options?.menuOptions, + { primary, secondary }, + options?.toolbarOptions?.primaryGroup + ); + super.update(primary, secondary); }; this._store.add(menu.onDidChange(update)); update(); diff --git a/src/vs/platform/actions/browser/floatingMenu.ts b/src/vs/platform/actions/browser/floatingMenu.ts index e6840b10e10..e7285146aa0 100644 --- a/src/vs/platform/actions/browser/floatingMenu.ts +++ b/src/vs/platform/actions/browser/floatingMenu.ts @@ -119,7 +119,7 @@ export class FloatingClickMenu extends AbstractFloatingClickMenu { const w = this.instantiationService.createInstance(FloatingClickWidget, action.label); const node = w.getDomNode(); this.options.container.appendChild(node); - disposable.add(toDisposable(() => this.options.container.removeChild(node))); + disposable.add(toDisposable(() => node.remove())); return w; } diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.css b/src/vs/platform/actions/browser/menuEntryActionViewItem.css index c5cba140485..7eb35af7e4b 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.css +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.css @@ -11,6 +11,20 @@ background-size: 16px; } +.monaco-action-bar .action-item.menu-entry.text-only .action-label { + color: var(--vscode-descriptionForeground); + overflow: hidden; + border-radius: 2px; +} + +.monaco-action-bar .action-item.menu-entry.text-only.use-comma:not(:last-of-type) .action-label::after { + content: ', '; +} + +.monaco-action-bar .action-item.menu-entry.text-only + .action-item:not(.text-only) > .monaco-dropdown .action-label { + color: var(--vscode-descriptionForeground); +} + .monaco-dropdown-with-default { display: flex !important; flex-direction: row; diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index da596a8fc6c..68380cddacf 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -31,6 +31,7 @@ import { assertType } from 'vs/base/common/types'; import { asCssVariable, selectBorder } from 'vs/platform/theme/common/colorRegistry'; import { defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { ResolvedKeybinding } from 'vs/base/common/keybindings'; export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[] }, primaryGroup?: string): void { const groups = menu.getActions(options); @@ -121,7 +122,7 @@ export interface IMenuEntryActionViewItemOptions { hoverDelegate?: IHoverDelegate; } -export class MenuEntryActionViewItem extends ActionViewItem { +export class MenuEntryActionViewItem extends ActionViewItem { private _wantsAltCommand: boolean = false; private readonly _itemClassDispose = this._register(new MutableDisposable()); @@ -129,7 +130,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { constructor( action: MenuItemAction, - options: IMenuEntryActionViewItemOptions | undefined, + protected _options: T | undefined, @IKeybindingService protected readonly _keybindingService: IKeybindingService, @INotificationService protected _notificationService: INotificationService, @IContextKeyService protected _contextKeyService: IContextKeyService, @@ -137,7 +138,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { @IContextMenuService protected _contextMenuService: IContextMenuService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService ) { - super(undefined, action, { icon: !!(action.class || action.item.icon), label: !action.class && !action.item.icon, draggable: options?.draggable, keybinding: options?.keybinding, hoverDelegate: options?.hoverDelegate }); + super(undefined, action, { icon: !!(action.class || action.item.icon), label: !action.class && !action.item.icon, draggable: _options?.draggable, keybinding: _options?.keybinding, hoverDelegate: _options?.hoverDelegate }); this._altKey = ModifierKeyEmitter.getInstance(); } @@ -285,6 +286,45 @@ export class MenuEntryActionViewItem extends ActionViewItem { } } +export interface ITextOnlyMenuEntryActionViewItemOptions extends IMenuEntryActionViewItemOptions { + conversational?: boolean; + useComma?: boolean; +} + +export class TextOnlyMenuEntryActionViewItem extends MenuEntryActionViewItem { + + override render(container: HTMLElement): void { + this.options.label = true; + this.options.icon = false; + super.render(container); + container.classList.add('text-only'); + container.classList.toggle('use-comma', this._options?.useComma ?? false); + } + + protected override updateLabel() { + const kb = this._keybindingService.lookupKeybinding(this._action.id, this._contextKeyService); + if (!kb) { + return super.updateLabel(); + } + if (this.label) { + const kb2 = TextOnlyMenuEntryActionViewItem._symbolPrintEnter(kb); + + if (this._options?.conversational) { + this.label.textContent = localize({ key: 'content2', comment: ['A label with keybindg like "ESC to dismiss"'] }, '{1} to {0}', this._action.label, kb2); + + } else { + this.label.textContent = localize({ key: 'content', comment: ['A label', 'A keybinding'] }, '{0} ({1})', this._action.label, kb2); + } + } + } + + private static _symbolPrintEnter(kb: ResolvedKeybinding) { + return kb.getLabel() + ?.replace(/\benter\b/gi, '\u23CE') + .replace(/\bEscape\b/gi, 'Esc'); + } +} + export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem { constructor( diff --git a/src/vs/platform/actions/browser/toolbar.ts b/src/vs/platform/actions/browser/toolbar.ts index 4857d4bc07b..a525f06ae9e 100644 --- a/src/vs/platform/actions/browser/toolbar.ts +++ b/src/vs/platform/actions/browser/toolbar.ts @@ -202,7 +202,8 @@ export class WorkbenchToolBar extends ToolBar { if (action instanceof MenuItemAction && action.menuKeybinding) { primaryActions.push(action.menuKeybinding); } else if (!(action instanceof SubmenuItemAction || action instanceof ToggleMenuAction)) { - primaryActions.push(createConfigureKeybindingAction(action.id, undefined, this._commandService, this._keybindingService)); + const isDisabled = action.id.startsWith('statusbaraction'); // We can't support keybinding configuration for scm statusbar actions + primaryActions.push(createConfigureKeybindingAction(this._commandService, this._keybindingService, action.id, undefined, !isDisabled)); } // -- Hide Actions -- diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 34617e04447..84f7ee4e84f 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -176,6 +176,8 @@ export class MenuId { static readonly InteractiveCellDelete = new MenuId('InteractiveCellDelete'); static readonly InteractiveCellExecute = new MenuId('InteractiveCellExecute'); static readonly InteractiveInputExecute = new MenuId('InteractiveInputExecute'); + static readonly InteractiveInputConfig = new MenuId('InteractiveInputConfig'); + static readonly ReplInputExecute = new MenuId('ReplInputExecute'); static readonly IssueReporter = new MenuId('IssueReporter'); static readonly NotebookToolbar = new MenuId('NotebookToolbar'); static readonly NotebookStickyScrollContext = new MenuId('NotebookStickyScrollContext'); @@ -214,6 +216,7 @@ export class MenuId { static readonly TerminalStickyScrollContext = new MenuId('TerminalStickyScrollContext'); static readonly WebviewContext = new MenuId('WebviewContext'); static readonly InlineCompletionsActions = new MenuId('InlineCompletionsActions'); + static readonly InlineEditsActions = new MenuId('InlineEditsActions'); static readonly InlineEditActions = new MenuId('InlineEditActions'); static readonly NewFile = new MenuId('NewFile'); static readonly MergeInput1Toolbar = new MenuId('MergeToolbar1Toolbar'); diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index 61fefa8e4ae..0b2f255d9be 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -248,7 +248,7 @@ class MenuInfo { const menuHide = createMenuHide(this._id, isMenuItem ? item.command : item, this._hiddenStates); if (isMenuItem) { // MenuItemAction - const menuKeybinding = createConfigureKeybindingAction(item.command.id, item.when, this._commandService, this._keybindingService); + const menuKeybinding = createConfigureKeybindingAction(this._commandService, this._keybindingService, item.command.id, item.when); (activeActions ??= []).push(new MenuItemAction(item.command, item.alt, options, menuHide, menuKeybinding, this._contextKeyService, this._commandService)); } else { // SubmenuItemAction @@ -442,10 +442,11 @@ function createMenuHide(menu: MenuId, command: ICommandAction | ISubmenuItem, st }; } -export function createConfigureKeybindingAction(commandId: string, when: ContextKeyExpression | undefined = undefined, commandService: ICommandService, keybindingService: IKeybindingService): IAction { +export function createConfigureKeybindingAction(commandService: ICommandService, keybindingService: IKeybindingService, commandId: string, when: ContextKeyExpression | undefined = undefined, enabled = true): IAction { return toAction({ id: `configureKeybinding/${commandId}`, label: localize('configure keybinding', "Configure Keybinding"), + enabled, run() { // Only set the when clause when there is no keybinding // It is possible that the action and the keybinding have different when clauses diff --git a/src/vs/platform/actions/test/common/menuService.test.ts b/src/vs/platform/actions/test/common/menuService.test.ts index 31e5d6c17be..8a3b2c421a9 100644 --- a/src/vs/platform/actions/test/common/menuService.test.ts +++ b/src/vs/platform/actions/test/common/menuService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { generateUuid } from 'vs/base/common/uuid'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 9fd5c045065..749cfa4e6e6 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { createHash } from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; diff --git a/src/vs/platform/checksum/test/node/checksumService.test.ts b/src/vs/platform/checksum/test/node/checksumService.test.ts index 3e56af64720..5e7e71cdd7a 100644 --- a/src/vs/platform/checksum/test/node/checksumService.test.ts +++ b/src/vs/platform/checksum/test/node/checksumService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/clipboard/browser/clipboardService.ts b/src/vs/platform/clipboard/browser/clipboardService.ts index 6cfe424e2dd..d4e156d94d1 100644 --- a/src/vs/platform/clipboard/browser/clipboardService.ts +++ b/src/vs/platform/clipboard/browser/clipboardService.ts @@ -134,7 +134,7 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer activeElement.focus(); } - activeDocument.body.removeChild(textArea); + textArea.remove(); } async readText(type?: string): Promise { diff --git a/src/vs/platform/commands/test/common/commands.test.ts b/src/vs/platform/commands/test/common/commands.test.ts index eeb3ed5da2a..f46f8e1a6ae 100644 --- a/src/vs/platform/commands/test/common/commands.test.ts +++ b/src/vs/platform/commands/test/common/commands.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { combinedDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index ed8e56d50c5..322e807f0a7 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -233,21 +233,24 @@ export interface IConfigurationNode { restrictedProperties?: string[]; } +export type ConfigurationDefaultSource = IExtensionInfo | string; +export type ConfigurationDefaultValueSource = ConfigurationDefaultSource | Map; + export interface IConfigurationDefaults { overrides: IStringDictionary; - source?: IExtensionInfo | string; + source?: ConfigurationDefaultSource; } export type IRegisteredConfigurationPropertySchema = IConfigurationPropertySchema & { defaultDefaultValue?: any; source?: IExtensionInfo; // Source of the Property - defaultValueSource?: IExtensionInfo | string; // Source of the Default Value + defaultValueSource?: ConfigurationDefaultValueSource; // Source of the Default Value }; export type IConfigurationDefaultOverride = { readonly value: any; - readonly source?: IExtensionInfo | string; // Source of the default override - readonly valuesSources?: Map; // Source of each value in default language overrides + readonly source?: ConfigurationDefaultValueSource; // Source of the default override + readonly valuesSources?: Map; // Source of each value in default language overrides }; export const allSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; @@ -351,13 +354,42 @@ class ConfigurationRegistry implements IConfigurationRegistry { if (OVERRIDE_PROPERTY_REGEX.test(key)) { const configurationDefaultOverride = this.configurationDefaultsOverrides.get(key); - const valuesSources = configurationDefaultOverride?.valuesSources ?? new Map(); - if (source) { - for (const configuration of Object.keys(overrides[key])) { - valuesSources.set(configuration, source); + const valuesSources = configurationDefaultOverride?.valuesSources ?? new Map(); + + const defaultValue = configurationDefaultOverride?.value || {}; + for (const configuration of Object.keys(overrides[key])) { + const overrideValue = overrides[key][configuration]; + + const isObjectSetting = types.isObject(overrideValue) && (types.isUndefined(defaultValue[configuration]) || types.isObject(defaultValue[configuration])); + if (isObjectSetting) { + // Objects are merged instead of overridden + defaultValue[configuration] = { ...(defaultValue[configuration] ?? {}), ...overrideValue }; + + // Track the source of each value in the object + if (source) { + let objectConfigurationSources = valuesSources.get(configuration); + if (!objectConfigurationSources) { + objectConfigurationSources = new Map(); + valuesSources.set(configuration, objectConfigurationSources); + } + if (!(objectConfigurationSources instanceof Map)) { + console.error('objectConfigurationSources is not a Map'); + continue; + } + + for (const objectKey in overrideValue) { + objectConfigurationSources.set(objectKey, source); + } + } + } else { + // Primitive values are overridden + defaultValue[configuration] = overrideValue; + if (source) { + valuesSources.set(configuration, source); + } } } - const defaultValue = { ...(configurationDefaultOverride?.value || {}), ...overrides[key] }; + this.configurationDefaultsOverrides.set(key, { source, value: defaultValue, valuesSources }); const plainKey = getLanguageTagSettingPlainKey(key); const property: IRegisteredConfigurationPropertySchema = { @@ -373,8 +405,43 @@ class ConfigurationRegistry implements IConfigurationRegistry { this.configurationProperties[key] = property; this.defaultLanguageConfigurationOverridesNode.properties![key] = property; } else { - this.configurationDefaultsOverrides.set(key, { value: overrides[key], source }); const property = this.configurationProperties[key]; + + const existingDefaultOverride = this.configurationDefaultsOverrides.get(key); + let existingDefaultValue = existingDefaultOverride?.value ?? property?.defaultDefaultValue; + + let newDefaultValue = overrides[key]; + let newDefaultValueSource: ConfigurationDefaultValueSource | undefined = source; + + const isObjectSetting = types.isObject(newDefaultValue) && ( + property !== undefined && property.type === 'object' || + property === undefined && (types.isUndefined(existingDefaultValue) || types.isObject(existingDefaultValue))); + + // If the default value is an object, merge the objects and store the source of each keys + if (isObjectSetting) { + if (!types.isObject(existingDefaultValue)) { + existingDefaultValue = {}; + } + + newDefaultValue = { ...existingDefaultValue, ...newDefaultValue }; + + newDefaultValueSource = existingDefaultOverride?.source ?? new Map(); + if (!(newDefaultValueSource instanceof Map)) { + console.error('defaultValueSource is not a Map'); + continue; + } + + for (const overrideObjectKey in overrides[key]) { + if (source) { + newDefaultValueSource.set(overrideObjectKey, source); + } else { + newDefaultValueSource.delete(overrideObjectKey); + } + } + } + + this.configurationDefaultsOverrides.set(key, { value: newDefaultValue, source: newDefaultValueSource }); + if (property) { this.updatePropertyDefaultValue(key, property); this.updateSchema(key, property); @@ -397,24 +464,87 @@ class ConfigurationRegistry implements IConfigurationRegistry { for (const { overrides, source } of defaultConfigurations) { for (const key in overrides) { - const configurationDefaultsOverride = this.configurationDefaultsOverrides.get(key); const id = types.isString(source) ? source : source?.id; - const configurationDefaultsOverrideSourceId = types.isString(configurationDefaultsOverride?.source) ? configurationDefaultsOverride?.source : configurationDefaultsOverride?.source?.id; - if (id !== configurationDefaultsOverrideSourceId) { + + const configurationDefaultsOverride = this.configurationDefaultsOverrides.get(key); + if (!configurationDefaultsOverride) { continue; } - bucket.add(key); - this.configurationDefaultsOverrides.delete(key); + if (OVERRIDE_PROPERTY_REGEX.test(key)) { - delete this.configurationProperties[key]; - delete this.defaultLanguageConfigurationOverridesNode.properties![key]; + for (const configuration of Object.keys(overrides[key])) { + const overrideValue = overrides[key][configuration]; + + if (types.isObject(overrideValue)) { + const configurationSource = configurationDefaultsOverride.valuesSources?.get(configuration) as Map | undefined; + + for (const overrideObjectKey of Object.keys(overrideValue)) { + const keySource = configurationSource?.get(overrideObjectKey); + const keySourceId = types.isString(keySource) ? keySource : keySource?.id; + if (keySourceId === id) { + configurationSource?.delete(overrideObjectKey); + delete configurationDefaultsOverride.value[configuration][overrideObjectKey]; + } + } + + if (Object.keys(configurationDefaultsOverride.value[configuration]).length === 0) { + delete configurationDefaultsOverride.value[configuration]; + configurationDefaultsOverride.valuesSources?.delete(configuration); + } + } else { + const configurationSource = configurationDefaultsOverride.valuesSources?.get(configuration) as string | IExtensionInfo | undefined; + + const keySourceId = types.isString(configurationSource) ? configurationSource : configurationSource?.id; + if (keySourceId === id) { + configurationDefaultsOverride.valuesSources?.delete(configuration); + delete configurationDefaultsOverride.value[configuration]; + } + } + } + // Remove language configuration if empty ({[css]: {}} => {}) + const languageValues = this.configurationDefaultsOverrides.get(key); + if (languageValues && Object.keys(languageValues.value).length === 0) { + this.configurationDefaultsOverrides.delete(key); + delete this.configurationProperties[key]; + delete this.defaultLanguageConfigurationOverridesNode.properties![key]; + } } else { + // If the default value is an object, remove the source of each key + if (configurationDefaultsOverride.source instanceof Map) { + + const keySources = configurationDefaultsOverride.source; + for (const objectKey in overrides[key]) { + const keySource = keySources.get(objectKey); + const keySourceId = types.isString(keySource) ? keySource : keySource?.id; + + if (keySourceId === id) { + keySources.delete(objectKey); + delete configurationDefaultsOverride.value[objectKey]; + } + } + + if (keySources.size === 0) { + this.configurationDefaultsOverrides.delete(key); + } + } + // Otherwise, remove the default value if the source matches + else { + const configurationDefaultsOverrideSourceId = types.isString(configurationDefaultsOverride.source) ? configurationDefaultsOverride.source : configurationDefaultsOverride.source?.id; + if (id !== configurationDefaultsOverrideSourceId) { + continue; // Another source is overriding this default value + } + + this.configurationDefaultsOverrides.delete(key); + + } const property = this.configurationProperties[key]; if (property) { this.updatePropertyDefaultValue(key, property); this.updateSchema(key, property); } } + + bucket.add(key); } } diff --git a/src/vs/platform/configuration/test/common/configuration.test.ts b/src/vs/platform/configuration/test/common/configuration.test.ts index e7b169e7341..1f709cf2c0f 100644 --- a/src/vs/platform/configuration/test/common/configuration.test.ts +++ b/src/vs/platform/configuration/test/common/configuration.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { merge, removeFromValueTree } from 'vs/platform/configuration/common/configuration'; import { mergeChanges } from 'vs/platform/configuration/common/configurationModels'; diff --git a/src/vs/platform/configuration/test/common/configurationModels.test.ts b/src/vs/platform/configuration/test/common/configurationModels.test.ts index 50b612d83d9..ab425c4fd30 100644 --- a/src/vs/platform/configuration/test/common/configurationModels.test.ts +++ b/src/vs/platform/configuration/test/common/configurationModels.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ResourceMap } from 'vs/base/common/map'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/configuration/test/common/configurationRegistry.test.ts b/src/vs/platform/configuration/test/common/configurationRegistry.test.ts index 9fc9e709322..24dc735cd11 100644 --- a/src/vs/platform/configuration/test/common/configurationRegistry.test.ts +++ b/src/vs/platform/configuration/test/common/configurationRegistry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -38,7 +38,7 @@ suite('ConfigurationRegistry', () => { assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, b: 2, c: 3 }); }); - test('configuration defaults - overrides defaults', async () => { + test('configuration defaults - merge object default overrides', async () => { configurationRegistry.registerConfiguration({ 'id': '_test_default', 'type': 'object', @@ -51,7 +51,7 @@ suite('ConfigurationRegistry', () => { configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 1, b: 2 } } }]); configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 2, c: 3 } } }]); - assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, c: 3 }); + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, b: 2, c: 3 }); }); test('registering multiple settings with same policy', async () => { @@ -79,4 +79,32 @@ suite('ConfigurationRegistry', () => { assert.ok(actual['policy1'] !== undefined); assert.ok(actual['policy2'] === undefined); }); + + test('configuration defaults - deregister merged object default override', async () => { + configurationRegistry.registerConfiguration({ + 'id': '_test_default', + 'type': 'object', + 'properties': { + 'config': { + 'type': 'object', + } + } + }); + + const overrides1 = [{ overrides: { 'config': { a: 1, b: 2 } }, source: 'source1' }]; + const overrides2 = [{ overrides: { 'config': { a: 2, c: 3 } }, source: 'source2' }]; + + configurationRegistry.registerDefaultConfigurations(overrides1); + configurationRegistry.registerDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, b: 2, c: 3 }); + + configurationRegistry.deregisterDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { b: 2 }); // TODO this should actualy equal overrides1 + + configurationRegistry.deregisterDefaultConfigurations(overrides1); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, {}); + }); }); diff --git a/src/vs/platform/configuration/test/common/configurationService.test.ts b/src/vs/platform/configuration/test/common/configurationService.test.ts index 880e49e8e48..0eff504b9bd 100644 --- a/src/vs/platform/configuration/test/common/configurationService.test.ts +++ b/src/vs/platform/configuration/test/common/configurationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { Schemas } from 'vs/base/common/network'; diff --git a/src/vs/platform/configuration/test/common/configurations.test.ts b/src/vs/platform/configuration/test/common/configurations.test.ts index 378107be505..eb02aec4f9a 100644 --- a/src/vs/platform/configuration/test/common/configurations.test.ts +++ b/src/vs/platform/configuration/test/common/configurations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { equals } from 'vs/base/common/objects'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -110,7 +110,7 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('a'), { b: { c: '2' } })); assert.ok(equals(actual.contents, { 'a': { b: { c: '2' } } })); - assert.deepStrictEqual(actual.keys, ['a.b', 'a.b.c']); + assert.deepStrictEqual(actual.keys.sort(), ['a.b', 'a.b.c']); }); test('Test registering the same property again', async () => { @@ -158,7 +158,7 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['[a]']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); }); @@ -191,7 +191,7 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { 'b': false, '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['b', '[a]']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]', 'b']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); }); @@ -227,7 +227,7 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { 'b': false, '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['[a]', 'b']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]', 'b']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); assert.deepStrictEqual(properties, ['b']); }); @@ -263,7 +263,7 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { 'b': false, '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['b', '[a]']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]', 'b']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); assert.deepStrictEqual(properties, ['[a]']); }); @@ -299,7 +299,7 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { 'b': false, '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['b', '[a]']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]', 'b']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); }); @@ -361,4 +361,53 @@ suite('DefaultConfiguration', () => { assert.deepStrictEqual(testObject.configurationModel.keys, ['b']); assert.strictEqual(testObject.configurationModel.getOverrideValue('b', 'a'), undefined); }); + + test('Test deregistering a merged language object setting', async () => { + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); + configurationRegistry.registerConfiguration({ + 'id': 'b', + 'order': 1, + 'title': 'b', + 'type': 'object', + 'properties': { + 'b': { + 'description': 'b', + 'type': 'object', + 'default': {}, + } + } + }); + const node1 = { + overrides: { + '[a]': { + 'b': { + 'aa': '1', + 'bb': '2' + } + } + }, + source: 'source1' + }; + + const node2 = { + overrides: { + '[a]': { + 'b': { + 'bb': '20', + 'cc': '30' + } + } + }, + source: 'source2' + }; + configurationRegistry.registerDefaultConfigurations([node1]); + configurationRegistry.registerDefaultConfigurations([node2]); + await testObject.initialize(); + configurationRegistry.deregisterDefaultConfigurations([node1]); + assert.ok(equals(testObject.configurationModel.getValue('[a]'), { 'b': { 'bb': '20', 'cc': '30' } })); + assert.ok(equals(testObject.configurationModel.contents, { '[a]': { 'b': { 'bb': '20', 'cc': '30' } }, 'b': {} })); + //assert.ok(equals(testObject.configurationModel.overrides, [{ '[a]': { 'b': { 'bb': '20', 'cc': '30' } } }])); TODO: Check this later + //assert.deepStrictEqual(testObject.configurationModel.keys.sort(), ['[a]', 'b']); + assert.ok(equals(testObject.configurationModel.getOverrideValue('b', 'a'), { 'bb': '20', 'cc': '30' })); + }); }); diff --git a/src/vs/platform/configuration/test/common/policyConfiguration.test.ts b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts index 94ee037eb5d..d3e44993618 100644 --- a/src/vs/platform/configuration/test/common/policyConfiguration.test.ts +++ b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { DefaultConfiguration, PolicyConfiguration } from 'vs/platform/configuration/common/configurations'; diff --git a/src/vs/platform/contextkey/test/browser/contextkey.test.ts b/src/vs/platform/contextkey/test/browser/contextkey.test.ts index b56d4b874da..d2301c19147 100644 --- a/src/vs/platform/contextkey/test/browser/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/browser/contextkey.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DeferredPromise } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 8f388fb13ee..2555701c1d8 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ContextKeyExpr, ContextKeyExpression, implies } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/platform/contextkey/test/common/parser.test.ts b/src/vs/platform/contextkey/test/common/parser.test.ts index c5be2596341..17bfa468ec9 100644 --- a/src/vs/platform/contextkey/test/common/parser.test.ts +++ b/src/vs/platform/contextkey/test/common/parser.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Parser } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/platform/contextkey/test/common/scanner.test.ts b/src/vs/platform/contextkey/test/common/scanner.test.ts index df897db9e8a..dacbfbebbdd 100644 --- a/src/vs/platform/contextkey/test/common/scanner.test.ts +++ b/src/vs/platform/contextkey/test/common/scanner.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Scanner, Token, TokenType } from 'vs/platform/contextkey/common/scanner'; diff --git a/src/vs/platform/diagnostics/common/diagnostics.ts b/src/vs/platform/diagnostics/common/diagnostics.ts index 7d5af4f92da..fb30b762cef 100644 --- a/src/vs/platform/diagnostics/common/diagnostics.ts +++ b/src/vs/platform/diagnostics/common/diagnostics.ts @@ -80,6 +80,8 @@ export interface WorkspaceStats { fileCount: number; maxFilesReached: boolean; launchConfigFiles: WorkspaceStatItem[]; + totalScanTime: number; + totalReaddirCount: number; } export interface PerformanceInfo { diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 7ca4d9c278c..26a715bfc19 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -9,6 +9,7 @@ import { Schemas } from 'vs/base/common/network'; import { basename, join } from 'vs/base/common/path'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { ProcessItem } from 'vs/base/common/processes'; +import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; import { virtualMachineHint } from 'vs/base/node/id'; import { IDirent, Promises as pfs } from 'vs/base/node/pfs'; @@ -60,11 +61,13 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P const MAX_FILES = 20000; - function collect(root: string, dir: string, filter: string[], token: { count: number; maxReached: boolean }): Promise { + function collect(root: string, dir: string, filter: string[], token: { count: number; maxReached: boolean; readdirCount: number }): Promise { const relativePath = dir.substring(root.length + 1); return Promises.withAsyncBody(async resolve => { let files: IDirent[]; + + token.readdirCount++; try { files = await pfs.readdir(dir, { withFileTypes: true }); } catch (error) { @@ -130,8 +133,8 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P } const statsPromise = Promises.withAsyncBody(async (resolve) => { - const token: { count: number; maxReached: boolean } = { count: 0, maxReached: false }; - + const token: { count: number; maxReached: boolean; readdirCount: number } = { count: 0, maxReached: false, readdirCount: 0 }; + const sw = new StopWatch(true); await collect(folder, folder, filter, token); const launchConfigs = await collectLaunchConfigs(folder); resolve({ @@ -139,7 +142,9 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P fileTypes: asSortedItems(fileTypes), fileCount: token.count, maxFilesReached: token.maxReached, - launchConfigFiles: launchConfigs + launchConfigFiles: launchConfigs, + totalScanTime: sw.elapsed(), + totalReaddirCount: token.readdirCount }); }); @@ -568,6 +573,23 @@ export class DiagnosticsService implements IDiagnosticsService { count: e.count }); }); + + // Workspace stats metadata + type WorkspaceStatsMetadataClassification = { + owner: 'jrieken'; + comment: 'Metadata about workspace metadata collection'; + duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How did it take to make workspace stats' }; + reachedLimit: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Did making workspace stats reach its limits' }; + fileCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How many files did workspace stats discover' }; + readdirCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How many readdir call were needed' }; + }; + type WorkspaceStatsMetadata = { + duration: number; + reachedLimit: boolean; + fileCount: number; + readdirCount: number; + }; + this.telemetryService.publicLog2('workspace.stats.metadata', { duration: stats.totalScanTime, reachedLimit: stats.maxFilesReached, fileCount: stats.fileCount, readdirCount: stats.totalReaddirCount }); } catch { // Report nothing if collecting metadata fails. } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 26b7d9c6937..16a942afe05 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as minimist from 'minimist'; +import minimist from 'minimist'; import { isWindows } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; diff --git a/src/vs/platform/environment/node/argvHelper.ts b/src/vs/platform/environment/node/argvHelper.ts index d8cefb6df67..a94fca911ea 100644 --- a/src/vs/platform/environment/node/argvHelper.ts +++ b/src/vs/platform/environment/node/argvHelper.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; diff --git a/src/vs/platform/environment/node/userDataPath.js b/src/vs/platform/environment/node/userDataPath.js index 92898523ed1..1e89f1fee06 100644 --- a/src/vs/platform/environment/node/userDataPath.js +++ b/src/vs/platform/environment/node/userDataPath.js @@ -10,8 +10,10 @@ 'use strict'; /** - * @typedef {import('../../environment/common/argv').NativeParsedArgs} NativeParsedArgs - * + * @import { NativeParsedArgs } from '../../environment/common/argv' + */ + + /** * @param {typeof import('path')} path * @param {typeof import('os')} os * @param {string} cwd diff --git a/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts b/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts index 78fd7354520..268f5ce52bb 100644 --- a/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts +++ b/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import product from 'vs/platform/product/common/product'; import { isLinux } from 'vs/base/common/platform'; diff --git a/src/vs/platform/environment/test/node/argv.test.ts b/src/vs/platform/environment/test/node/argv.test.ts index a188b52b711..a82be9607d0 100644 --- a/src/vs/platform/environment/test/node/argv.test.ts +++ b/src/vs/platform/environment/test/node/argv.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { formatOptions, Option, OptionDescriptions, Subcommand, parseArgs, ErrorReporter } from 'vs/platform/environment/node/argv'; import { addArg } from 'vs/platform/environment/node/argvHelper'; diff --git a/src/vs/platform/environment/test/node/environmentService.test.ts b/src/vs/platform/environment/test/node/environmentService.test.ts index ffe418fc702..6f256621040 100644 --- a/src/vs/platform/environment/test/node/environmentService.test.ts +++ b/src/vs/platform/environment/test/node/environmentService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { parseExtensionHostDebugPort } from 'vs/platform/environment/common/environmentService'; import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; diff --git a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts index 68128a214fd..81db8b47267 100644 --- a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts +++ b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; import { flakySuite } from 'vs/base/test/common/testUtils'; @@ -19,7 +19,7 @@ flakySuite('Native Modules (all platforms)', () => { }); test('native-is-elevated', async () => { - const isElevated = await import('native-is-elevated'); + const isElevated = (await import('native-is-elevated')).default; assert.ok(typeof isElevated === 'function', testErrorMessage('native-is-elevated ')); const result = isElevated(); diff --git a/src/vs/platform/environment/test/node/userDataPath.test.ts b/src/vs/platform/environment/test/node/userDataPath.test.ts index 644260cff8f..72278e46ac0 100644 --- a/src/vs/platform/environment/test/node/userDataPath.test.ts +++ b/src/vs/platform/environment/test/node/userDataPath.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; import { getUserDataPath } from 'vs/platform/environment/node/userDataPath'; diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 2ac86759fb9..284658e08d6 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -18,10 +18,12 @@ import { IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError, IProductVersion, ExtensionGalleryErrorCode, - EXTENSION_INSTALL_SOURCE_CONTEXT + EXTENSION_INSTALL_SOURCE_CONTEXT, + DidUpdateExtensionMetadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { areApiProposalsCompatible } from 'vs/platform/extensions/common/extensionValidator'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -73,7 +75,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl protected _onDidUninstallExtension = this._register(new Emitter()); get onDidUninstallExtension() { return this._onDidUninstallExtension.event; } - protected readonly _onDidUpdateExtensionMetadata = this._register(new Emitter()); + protected readonly _onDidUpdateExtensionMetadata = this._register(new Emitter()); get onDidUpdateExtensionMetadata() { return this._onDidUpdateExtensionMetadata.event; } private readonly participants: IExtensionManagementParticipant[] = []; @@ -129,7 +131,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const compatible = await this.checkAndGetCompatibleVersion(extension, !!options?.installGivenVersion, !!options?.installPreReleaseVersion, options.productVersion ?? { version: this.productService.version, date: this.productService.date }); installableExtensions.push({ ...compatible, options }); } catch (error) { - results.push({ identifier: extension.identifier, operation: InstallOperation.Install, source: extension, error }); + results.push({ identifier: extension.identifier, operation: InstallOperation.Install, source: extension, error, profileLocation: options.profileLocation ?? this.getCurrentExtensionsManifestLocation() }); } })); @@ -160,7 +162,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const existing = (await this.getInstalled(ExtensionType.User, profile.extensionsResource)) .find(e => areSameExtensions(e.identifier, extension.identifier)); if (existing) { - this._onDidUpdateExtensionMetadata.fire(existing); + this._onDidUpdateExtensionMetadata.fire({ local: existing, profileLocation: profile.extensionsResource }); } else { this._onDidUninstallExtension.fire({ identifier: extension.identifier, profileLocation: profile.extensionsResource }); } @@ -205,7 +207,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const key = `${getGalleryExtensionId(manifest.publisher, manifest.name)}-${options.profileLocation.toString()}`; installingExtensionsMap.set(key, { task: installExtensionTask, root }); this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension, profileLocation: options.profileLocation }); - this.logService.info('Installing extension:', installExtensionTask.identifier.id, options.profileLocation.toString()); + this.logService.info('Installing extension:', installExtensionTask.identifier.id, options); // only cache gallery extensions tasks if (!URI.isUri(extension)) { this.installingExtensions.set(getInstallExtensionTaskKey(extension, options.profileLocation), { task: installExtensionTask, waitingTasks: [] }); @@ -547,6 +549,10 @@ export abstract class AbstractExtensionManagementService extends Disposable impl compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease, productVersion); if (!compatibleExtension) { + const incompatibleApiProposalsMessages: string[] = []; + if (!areApiProposalsCompatible(extension.properties.enabledApiProposals ?? [], incompatibleApiProposalsMessages)) { + throw new ExtensionManagementError(nls.localize('incompatibleAPI', "Can't install '{0}' extension. {1}", extension.displayName ?? extension.identifier.id, incompatibleApiProposalsMessages[0]), ExtensionManagementErrorCode.IncompatibleApi); + } /** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */ if (!installPreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) { throw new ExtensionManagementError(nls.localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.displayName ?? extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound); @@ -784,7 +790,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract reinstallFromGallery(extension: ILocalExtension): Promise; abstract cleanUp(): Promise; - abstract updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise; + abstract updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI): Promise; protected abstract getCurrentExtensionsManifestLocation(): URI; protected abstract createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask; diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index c2b32903850..522e1c37e52 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -18,7 +18,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; -import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; +import { areApiProposalsCompatible, isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -209,6 +209,7 @@ const PropertyType = { ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack', Engine: 'Microsoft.VisualStudio.Code.Engine', PreRelease: 'Microsoft.VisualStudio.Code.PreRelease', + EnabledApiProposals: 'Microsoft.VisualStudio.Code.EnabledApiProposals', LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages', WebExtension: 'Microsoft.VisualStudio.Code.WebExtension', SponsorLink: 'Microsoft.VisualStudio.Code.SponsorLink', @@ -430,6 +431,12 @@ function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean { return values.length > 0 && values[0].value === 'true'; } +function getEnabledApiProposals(version: IRawGalleryExtensionVersion): string[] { + const values = version.properties ? version.properties.filter(p => p.key === PropertyType.EnabledApiProposals) : []; + const value = (values.length > 0 && values[0].value) || ''; + return value ? value.split(',') : []; +} + function getLocalizedLanguages(version: IRawGalleryExtensionVersion): string[] { const values = version.properties ? version.properties.filter(p => p.key === PropertyType.LocalizedLanguages) : []; const value = (values.length > 0 && values[0].value) || ''; @@ -548,6 +555,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller dependencies: getExtensions(version, PropertyType.Dependency), extensionPack: getExtensions(version, PropertyType.ExtensionPack), engine: getEngine(version), + enabledApiProposals: getEnabledApiProposals(version), localizedLanguages: getLocalizedLanguages(version), targetPlatform: getTargetPlatformForExtensionVersion(version), isPreReleaseVersion: isPreReleaseVersion(version) @@ -579,6 +587,7 @@ interface IRawExtensionsControlManifest { additionalInfo?: string; }>; search?: ISearchPrefferedResults[]; + extensionsEnabledWithPreRelease?: string[]; } abstract class AbstractExtensionGalleryService implements IExtensionGalleryService { @@ -590,6 +599,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi private readonly extensionsControlUrl: string | undefined; private readonly commonHeadersPromise: Promise>; + private readonly extensionsEnabledWithApiProposalVersion: string[]; constructor( storageService: IStorageService | undefined, @@ -606,6 +616,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.extensionsGalleryUrl = isPPEEnabled ? config.servicePPEUrl : config?.serviceUrl; this.extensionsGallerySearchUrl = isPPEEnabled ? undefined : config?.searchUrl; this.extensionsControlUrl = config?.controlUrl; + this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? []; this.commonHeadersPromise = resolveMarketplaceHeaders( productService.version, productService, @@ -709,7 +720,26 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } engine = manifest.engines.vscode; } - return isEngineValid(engine, productVersion.version, productVersion.date); + + if (!isEngineValid(engine, productVersion.version, productVersion.date)) { + return false; + } + + if (!this.areApiProposalsCompatible(extension.identifier, extension.properties.enabledApiProposals)) { + return false; + } + + return true; + } + + private areApiProposalsCompatible(extensionIdentifier: IExtensionIdentifier, enabledApiProposals: string[] | undefined): boolean { + if (!enabledApiProposals) { + return true; + } + if (!this.extensionsEnabledWithApiProposalVersion.includes(extensionIdentifier.id.toLowerCase())) { + return true; + } + return areApiProposalsCompatible(enabledApiProposals); } private async isValidVersion(extension: string, rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { @@ -920,7 +950,18 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi continue; } // Allow any version if includePreRelease flag is set otherwise only release versions are allowed - if (await this.isValidVersion(getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform, criteria.productVersion)) { + if (await this.isValidVersion( + extensionIdentifier.id, + rawGalleryExtensionVersion, + includePreRelease ? 'any' : 'release', + criteria.compatible, + allTargetPlatforms, + criteria.targetPlatform, + criteria.productVersion) + ) { + if (criteria.compatible && !this.areApiProposalsCompatible(extensionIdentifier, getEnabledApiProposals(rawGalleryExtensionVersion))) { + return null; + } return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, queryContext); } if (version && rawGalleryExtensionVersion.version === version) { @@ -1142,15 +1183,15 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return ''; } - async getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { + async getAllCompatibleVersions(extensionIdentifier: IExtensionIdentifier, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { let query = new Query() .withFlags(Flags.IncludeVersions, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties) .withPage(1, 1); - if (extension.identifier.uuid) { - query = query.withFilter(FilterType.ExtensionId, extension.identifier.uuid); + if (extensionIdentifier.uuid) { + query = query.withFilter(FilterType.ExtensionId, extensionIdentifier.uuid); } else { - query = query.withFilter(FilterType.ExtensionName, extension.identifier.id); + query = query.withFilter(FilterType.ExtensionName, extensionIdentifier.id); } const { galleryExtensions } = await this.queryRawGalleryExtensions(query, CancellationToken.None); @@ -1166,7 +1207,15 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const validVersions: IRawGalleryExtensionVersion[] = []; await Promise.all(galleryExtensions[0].versions.map(async (version) => { try { - if (await this.isValidVersion(extension.identifier.id, version, includePreRelease ? 'any' : 'release', true, allTargetPlatforms, targetPlatform)) { + if ( + (await this.isValidVersion( + extensionIdentifier.id, + version, includePreRelease ? 'any' : 'release', + true, + allTargetPlatforms, + targetPlatform)) + && this.areApiProposalsCompatible(extensionIdentifier, getEnabledApiProposals(version)) + ) { validVersions.push(version); } } catch (error) { /* Ignore error and skip version */ } @@ -1267,6 +1316,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const malicious: IExtensionIdentifier[] = []; const deprecated: IStringDictionary = {}; const search: ISearchPrefferedResults[] = []; + const extensionsEnabledWithPreRelease: string[] = []; if (result) { for (const id of result.malicious) { malicious.push({ id }); @@ -1298,9 +1348,14 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi search.push(s); } } + if (Array.isArray(result.extensionsEnabledWithPreRelease)) { + for (const id of result.extensionsEnabledWithPreRelease) { + extensionsEnabledWithPreRelease.push(id.toLowerCase()); + } + } } - return { malicious, deprecated, search }; + return { malicious, deprecated, search, extensionsEnabledWithPreRelease }; } } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 183f2582871..68f21988fc4 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -159,6 +159,7 @@ export interface IGalleryExtensionProperties { dependencies?: string[]; extensionPack?: string[]; engine?: string; + enabledApiProposals?: string[]; localizedLanguages?: string[]; targetPlatform: TargetPlatform; isPreReleaseVersion: boolean; @@ -326,6 +327,7 @@ export interface IExtensionsControlManifest { readonly malicious: IExtensionIdentifier[]; readonly deprecated: IStringDictionary; readonly search: ISearchPrefferedResults[]; + readonly extensionsEnabledWithPreRelease?: string[]; } export const enum InstallOperation { @@ -367,7 +369,7 @@ export interface IExtensionGalleryService { getExtensions(extensionInfos: ReadonlyArray, options: IExtensionQueryOptions, token: CancellationToken): Promise; isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; - getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; + getAllCompatibleVersions(extensionIdentifier: IExtensionIdentifier, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise; downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise; reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise; @@ -381,7 +383,7 @@ export interface IExtensionGalleryService { export interface InstallExtensionEvent { readonly identifier: IExtensionIdentifier; readonly source: URI | IGalleryExtension; - readonly profileLocation?: URI; + readonly profileLocation: URI; readonly applicationScoped?: boolean; readonly workspaceScoped?: boolean; } @@ -393,14 +395,14 @@ export interface InstallExtensionResult { readonly local?: ILocalExtension; readonly error?: Error; readonly context?: IStringDictionary; - readonly profileLocation?: URI; + readonly profileLocation: URI; readonly applicationScoped?: boolean; readonly workspaceScoped?: boolean; } export interface UninstallExtensionEvent { readonly identifier: IExtensionIdentifier; - readonly profileLocation?: URI; + readonly profileLocation: URI; readonly applicationScoped?: boolean; readonly workspaceScoped?: boolean; } @@ -408,11 +410,16 @@ export interface UninstallExtensionEvent { export interface DidUninstallExtensionEvent { readonly identifier: IExtensionIdentifier; readonly error?: string; - readonly profileLocation?: URI; + readonly profileLocation: URI; readonly applicationScoped?: boolean; readonly workspaceScoped?: boolean; } +export interface DidUpdateExtensionMetadata { + readonly profileLocation: URI; + readonly local: ILocalExtension; +} + export const enum ExtensionGalleryErrorCode { Timeout = 'Timeout', Cancelled = 'Cancelled', @@ -432,12 +439,14 @@ export const enum ExtensionManagementErrorCode { Deprecated = 'Deprecated', Malicious = 'Malicious', Incompatible = 'Incompatible', + IncompatibleApi = 'IncompatibleApi', IncompatibleTargetPlatform = 'IncompatibleTargetPlatform', ReleaseVersionNotFound = 'ReleaseVersionNotFound', Invalid = 'Invalid', Download = 'Download', DownloadSignature = 'DownloadSignature', DownloadFailedWriting = ExtensionGalleryErrorCode.DownloadFailedWriting, + UpdateExistingMetadata = 'UpdateExistingMetadata', UpdateMetadata = 'UpdateMetadata', Extract = 'Extract', Scanning = 'Scanning', @@ -487,7 +496,14 @@ export type InstallOptions = { */ context?: IStringDictionary; }; -export type UninstallOptions = { readonly donotIncludePack?: boolean; readonly donotCheckDependents?: boolean; readonly versionOnly?: boolean; readonly remove?: boolean; readonly profileLocation?: URI }; + +export type UninstallOptions = { + readonly profileLocation?: URI; + readonly donotIncludePack?: boolean; + readonly donotCheckDependents?: boolean; + readonly versionOnly?: boolean; + readonly remove?: boolean; +}; export interface IExtensionManagementParticipant { postInstall(local: ILocalExtension, source: URI | IGalleryExtension, options: InstallOptions, token: CancellationToken): Promise; @@ -504,7 +520,7 @@ export interface IExtensionManagementService { onDidInstallExtensions: Event; onUninstallExtension: Event; onDidUninstallExtension: Event; - onDidUpdateExtensionMetadata: Event; + onDidUpdateExtensionMetadata: Event; zip(extension: ILocalExtension): Promise; unzip(zipLocation: URI): Promise; @@ -521,7 +537,7 @@ export interface IExtensionManagementService { getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; getExtensionsControlManifest(): Promise; copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; - updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise; + updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI): Promise; download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts b/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts index 9241f4170ab..17c76f6fe70 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts @@ -146,7 +146,7 @@ export class ExtensionManagementCLI { if (areSameExtensions(oldVersion.identifier, newVersion.identifier) && gt(newVersion.version, oldVersion.manifest.version)) { extensionsToUpdate.push({ extension: newVersion, - options: { operation: InstallOperation.Update, installPreReleaseVersion: oldVersion.preRelease, profileLocation } + options: { operation: InstallOperation.Update, installPreReleaseVersion: oldVersion.preRelease, profileLocation, isApplicationScoped: oldVersion.isApplicationScoped } }); } } @@ -224,7 +224,7 @@ export class ExtensionManagementCLI { } extensionsToInstall.push({ extension: gallery, - options: { ...installOptions, installGivenVersion: !!version }, + options: { ...installOptions, installGivenVersion: !!version, isApplicationScoped: installedExtension?.isApplicationScoped }, }); })); diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 6c3e289db7d..75cf3eb91ae 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,7 +9,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion, DidUpdateExtensionMetadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI; @@ -43,7 +43,7 @@ export class ExtensionManagementChannel implements IServerChannel { onDidInstallExtensions: Event; onUninstallExtension: Event; onDidUninstallExtension: Event; - onDidUpdateExtensionMetadata: Event; + onDidUpdateExtensionMetadata: Event; constructor(private service: IExtensionManagementService, private getUriTransformer: (requestContext: any) => IURITransformer | null) { this.onInstallExtension = Event.buffer(service.onInstallExtension, true); @@ -89,7 +89,12 @@ export class ExtensionManagementChannel implements IServerChannel { }); } case 'onDidUpdateExtensionMetadata': { - return Event.map(this.onDidUpdateExtensionMetadata, e => transformOutgoingExtension(e, uriTransformer)); + return Event.map(this.onDidUpdateExtensionMetadata, e => { + return { + local: transformOutgoingExtension(e.local, uriTransformer), + profileLocation: transformOutgoingURI(e.profileLocation, uriTransformer) + }; + }); } } @@ -168,7 +173,11 @@ export class ExtensionManagementChannel implements IServerChannel { } } -export type ExtensionEventResult = InstallExtensionEvent | InstallExtensionResult | UninstallExtensionEvent | DidUninstallExtensionEvent; +export interface ExtensionEventResult { + readonly profileLocation: URI; + readonly local?: ILocalExtension; + readonly applicationScoped?: boolean; +} export class ExtensionManagementChannelClient extends Disposable implements IExtensionManagementService { @@ -186,7 +195,7 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt private readonly _onDidUninstallExtension = this._register(new Emitter()); get onDidUninstallExtension() { return this._onDidUninstallExtension.event; } - private readonly _onDidUpdateExtensionMetadata = this._register(new Emitter()); + private readonly _onDidUpdateExtensionMetadata = this._register(new Emitter()); get onDidUpdateExtensionMetadata() { return this._onDidUpdateExtensionMetadata.event; } constructor(private readonly channel: IChannel) { @@ -195,16 +204,12 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt this._register(this.channel.listen('onDidInstallExtensions')(results => this.fireEvent(this._onDidInstallExtensions, results.map(e => ({ ...e, local: e.local ? transformIncomingExtension(e.local, null) : e.local, source: this.isUriComponents(e.source) ? URI.revive(e.source) : e.source, profileLocation: URI.revive(e.profileLocation) }))))); this._register(this.channel.listen('onUninstallExtension')(e => this.fireEvent(this._onUninstallExtension, { ...e, profileLocation: URI.revive(e.profileLocation) }))); this._register(this.channel.listen('onDidUninstallExtension')(e => this.fireEvent(this._onDidUninstallExtension, { ...e, profileLocation: URI.revive(e.profileLocation) }))); - this._register(this.channel.listen('onDidUpdateExtensionMetadata')(e => this._onDidUpdateExtensionMetadata.fire(transformIncomingExtension(e, null)))); + this._register(this.channel.listen('onDidUpdateExtensionMetadata')(e => this.fireEvent(this._onDidUpdateExtensionMetadata, { profileLocation: URI.revive(e.profileLocation), local: transformIncomingExtension(e.local, null) }))); } - protected fireEvent(event: Emitter, data: InstallExtensionEvent): void; - protected fireEvent(event: Emitter, data: InstallExtensionResult[]): void; - protected fireEvent(event: Emitter, data: UninstallExtensionEvent): void; - protected fireEvent(event: Emitter, data: DidUninstallExtensionEvent): void; - protected fireEvent(event: Emitter, data: ExtensionEventResult): void; - protected fireEvent(event: Emitter, data: ExtensionEventResult[]): void; - protected fireEvent(event: Emitter, data: E): void { + protected fireEvent(event: Emitter, data: E): void; + protected fireEvent(event: Emitter, data: E[]): void; + protected fireEvent(event: Emitter, data: E | E[]): void { event.fire(data); } diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index bcfe2f34bb8..812a9bcd706 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -24,7 +24,7 @@ import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IProductVersion, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap, parseEnabledApiProposalNames } from 'vs/platform/extensions/common/extensions'; import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -566,14 +566,19 @@ type NlsConfiguration = { class ExtensionsScanner extends Disposable { + private readonly extensionsEnabledWithApiProposalVersion: string[]; + constructor( private readonly obsoleteFile: URI, @IExtensionsProfileScannerService protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @IFileService protected readonly fileService: IFileService, + @IProductService productService: IProductService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, @ILogService protected readonly logService: ILogService ) { super(); + this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? []; } async scanExtensions(input: ExtensionScannerInput): Promise { @@ -708,7 +713,7 @@ class ExtensionsScanner extends Disposable { const type = metadata?.isSystem ? ExtensionType.System : input.type; const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin; manifest = await this.translateManifest(input.location, manifest, ExtensionScannerInput.createNlsConfiguration(input)); - const extension: IRelaxedScannedExtension = { + let extension: IRelaxedScannedExtension = { type, identifier, manifest, @@ -720,7 +725,13 @@ class ExtensionsScanner extends Disposable { isValid: true, validations: [] }; - return input.validate ? this.validate(extension, input) : extension; + if (input.validate) { + extension = this.validate(extension, input); + } + if (manifest.enabledApiProposals && this.extensionsEnabledWithApiProposalVersion.includes(id.toLowerCase())) { + manifest.enabledApiProposals = parseEnabledApiProposalNames([...manifest.enabledApiProposals]); + } + return extension; } } catch (e) { if (input.type !== ExtensionType.System) { @@ -732,8 +743,8 @@ class ExtensionsScanner extends Disposable { validate(extension: IRelaxedScannedExtension, input: ExtensionScannerInput): IRelaxedScannedExtension { let isValid = true; + const validateApiVersion = this.environmentService.isBuilt && this.extensionsEnabledWithApiProposalVersion.includes(extension.identifier.id.toLowerCase()); // --- Start Positron --- - const validations: [Severity, string][] = []; // Validate extension manifest for VS Code fields @@ -742,7 +753,8 @@ class ExtensionsScanner extends Disposable { input.productDate, input.location, extension.manifest, - extension.isBuiltin)); + extension.isBuiltin, + validateApiVersion)); // Validate extension manifest for Positron fields (i.e. engine: positron) validations.push(...validatePositronExtensionManifest( @@ -764,7 +776,7 @@ class ExtensionsScanner extends Disposable { return extension; } - async scanExtensionManifest(extensionLocation: URI): Promise { + private async scanExtensionManifest(extensionLocation: URI): Promise { const manifestLocation = joinPath(extensionLocation, 'package.json'); let content; try { @@ -953,9 +965,11 @@ class CachedExtensionsScanner extends ExtensionsScanner { @IExtensionsProfileScannerService extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService uriIdentityService: IUriIdentityService, @IFileService fileService: IFileService, + @IProductService productService: IProductService, + @IEnvironmentService environmentService: IEnvironmentService, @ILogService logService: ILogService ) { - super(obsoleteFile, extensionsProfileScannerService, uriIdentityService, fileService, logService); + super(obsoleteFile, extensionsProfileScannerService, uriIdentityService, fileService, productService, environmentService, logService); } override async scanExtensions(input: ExtensionScannerInput): Promise { @@ -963,7 +977,7 @@ class CachedExtensionsScanner extends ExtensionsScanner { const cacheContents = await this.readExtensionCache(cacheFile); this.input = input; if (cacheContents && cacheContents.input && ExtensionScannerInput.equals(cacheContents.input, this.input)) { - this.logService.debug('Using cached extensions scan result', input.location.toString()); + this.logService.debug('Using cached extensions scan result', input.type === ExtensionType.System ? 'system' : 'user', input.location.toString()); this.cacheValidatorThrottler.trigger(() => this.validateCache()); return cacheContents.result.map((extension) => { // revive URI object diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 275d5c3c5c5..29613dba832 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -12,7 +12,7 @@ import { CancellationError, getErrorMessage } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ResourceSet } from 'vs/base/common/map'; +import { ResourceMap, ResourceSet } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; @@ -186,7 +186,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return extensionsToInstall; } - async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource): Promise { + async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI): Promise { this.logService.trace('ExtensionManagementService#updateMetadata', local.identifier.id); if (metadata.isPreReleaseVersion) { metadata.preRelease = true; @@ -204,7 +204,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } local = await this.extensionsScanner.updateMetadata(local, metadata, profileLocation); this.manifestCache.invalidate(profileLocation); - this._onDidUpdateExtensionMetadata.fire(local); + this._onDidUpdateExtensionMetadata.fire({ local, profileLocation }); return local; } @@ -484,6 +484,17 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } } +type UpdateMetadataErrorClassification = { + owner: 'sandy081'; + comment: 'Update metadata error'; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension identifier' }; + code?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' }; +}; +type UpdateMetadataErrorEvent = { + extensionId: string; + code?: string; +}; + export class ExtensionsScanner extends Disposable { private readonly uninstalledResource: URI; @@ -492,12 +503,16 @@ export class ExtensionsScanner extends Disposable { private readonly _onExtract = this._register(new Emitter()); readonly onExtract = this._onExtract.event; + private scanAllExtensionPromise = new ResourceMap>(); + private scanUserExtensionsPromise = new ResourceMap>(); + constructor( private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise, @IFileService private readonly fileService: IFileService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, ) { super(); @@ -515,9 +530,21 @@ export class ExtensionsScanner extends Disposable { const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; let scannedExtensions: IScannedExtension[] = []; if (type === null || type === ExtensionType.System) { - scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions({ includeInvalid: true }, userScanOptions, false)); + let scanAllExtensionsPromise = this.scanAllExtensionPromise.get(profileLocation); + if (!scanAllExtensionsPromise) { + scanAllExtensionsPromise = this.extensionsScannerService.scanAllExtensions({ includeInvalid: true, useCache: true }, userScanOptions, false) + .finally(() => this.scanAllExtensionPromise.delete(profileLocation)); + this.scanAllExtensionPromise.set(profileLocation, scanAllExtensionsPromise); + } + scannedExtensions.push(...await scanAllExtensionsPromise); } else if (type === ExtensionType.User) { - scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(userScanOptions)); + let scanUserExtensionsPromise = this.scanUserExtensionsPromise.get(profileLocation); + if (!scanUserExtensionsPromise) { + scanUserExtensionsPromise = this.extensionsScannerService.scanUserExtensions(userScanOptions) + .finally(() => this.scanUserExtensionsPromise.delete(profileLocation)); + this.scanUserExtensionsPromise.set(profileLocation, scanUserExtensionsPromise); + } + scannedExtensions.push(...await scanUserExtensionsPromise); } scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); @@ -567,7 +594,8 @@ export class ExtensionsScanner extends Disposable { try { await this.extensionsScannerService.updateMetadata(extensionLocation, metadata); } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); + this.telemetryService.publicLog2('extension:extract', { extensionId: extensionKey.id, code: `${toFileOperationResult(error)}` }); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateExistingMetadata); } } else { try { @@ -587,6 +615,7 @@ export class ExtensionsScanner extends Disposable { try { await this.extensionsScannerService.updateMetadata(tempLocation, metadata); } catch (error) { + this.telemetryService.publicLog2('extension:extract', { extensionId: extensionKey.id, code: `${toFileOperationResult(error)}` }); throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); } @@ -642,6 +671,7 @@ export class ExtensionsScanner extends Disposable { await this.extensionsScannerService.updateMetadata(local.location, metadata); } } catch (error) { + this.telemetryService.publicLog2('extension:extract', { extensionId: local.identifier.id, code: `${toFileOperationResult(error)}` }); throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); } return this.scanLocalExtension(local.location, local.type, profileLocation); @@ -759,7 +789,7 @@ export class ExtensionsScanner extends Disposable { } } - private async scanLocalExtension(location: URI, type: ExtensionType, profileLocation?: URI): Promise { + async scanLocalExtension(location: URI, type: ExtensionType, profileLocation?: URI): Promise { try { if (profileLocation) { const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation }); @@ -912,13 +942,9 @@ class InstallExtensionInProfileTask extends AbstractExtensionTask { - const installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); - return installed.find(i => areSameExtensions(i.identifier, this.identifier)); - } - protected async doRun(token: CancellationToken): Promise { - const existingExtension = await this.getExistingExtension(); + const installed = await this.extensionsScanner.scanExtensions(ExtensionType.User, this.options.profileLocation, this.options.productVersion); + const existingExtension = installed.find(i => areSameExtensions(i.identifier, this.identifier)); if (existingExtension) { this._operation = InstallOperation.Update; } @@ -945,16 +971,15 @@ class InstallExtensionInProfileTask extends AbstractExtensionTask; } -export const enum ExtensionSignatureVerificationCode { - 'None' = 'None', +export enum ExtensionSignatureVerificationCode { + 'Success' = 'Success', 'RequiredArgumentMissing' = 'RequiredArgumentMissing', 'InvalidArgument' = 'InvalidArgument', 'PackageIsUnreadable' = 'PackageIsUnreadable', @@ -51,7 +51,6 @@ export const enum ExtensionSignatureVerificationCode { 'SignatureArchiveIsInvalidZip' = 'SignatureArchiveIsInvalidZip', 'SignatureArchiveHasSameSignatureFile' = 'SignatureArchiveHasSameSignatureFile', - 'Success' = 'Success', 'PackageIntegrityCheckFailed' = 'PackageIntegrityCheckFailed', 'SignatureIsInvalid' = 'SignatureIsInvalid', 'SignatureManifestIsInvalid' = 'SignatureManifestIsInvalid', diff --git a/src/vs/platform/extensionManagement/test/common/configRemotes.test.ts b/src/vs/platform/extensionManagement/test/common/configRemotes.test.ts index ce93c6e73d7..178293d8874 100644 --- a/src/vs/platform/extensionManagement/test/common/configRemotes.test.ts +++ b/src/vs/platform/extensionManagement/test/common/configRemotes.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getDomainsOfRemotes, getRemotes } from 'vs/platform/extensionManagement/common/configRemotes'; diff --git a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts index cebafab4714..a4c9afd9c58 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { isUUID } from 'vs/base/common/uuid'; diff --git a/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts b/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts index d0813771c18..1c3d62f7f0b 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionKey } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; diff --git a/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts b/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts index a3c2603a1eb..e23d69b9371 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { deepClone } from 'vs/base/common/objects'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILocalizedString } from 'vs/platform/action/common/action'; diff --git a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts index 5e71b85714e..d94305a83fc 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { VSBuffer } from 'vs/base/common/buffer'; import { joinPath } from 'vs/base/common/resources'; diff --git a/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts index e803de72c79..bae93bde803 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index b2ba53d730c..7c3e06d5aa9 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { dirname, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index cee5eaeedef..5fae8b4b1e2 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -8,7 +8,8 @@ import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import * as semver from 'vs/base/common/semver/semver'; -import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest, parseApiProposals } from 'vs/platform/extensions/common/extensions'; +import { allApiProposals } from 'vs/platform/extensions/common/extensionsApiProposals'; export interface IParsedVersion { hasCaret: boolean; @@ -239,7 +240,7 @@ export function isValidVersion(_inputVersion: string | INormalizedVersion, _inpu type ProductDate = string | Date | undefined; -export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean): readonly [Severity, string][] { +export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, validateApiVersion: boolean): readonly [Severity, string][] { const validations: [Severity, string][] = []; if (typeof extensionManifest.publisher !== 'undefined' && typeof extensionManifest.publisher !== 'string') { validations.push([Severity.Error, nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.")]); @@ -314,12 +315,22 @@ export function validateExtensionManifest(productVersion: string, productDate: P } const notices: string[] = []; - const isValid = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices); - if (!isValid) { + const validExtensionVersion = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices); + if (!validExtensionVersion) { for (const notice of notices) { validations.push([Severity.Error, notice]); } } + + if (validateApiVersion && extensionManifest.enabledApiProposals?.length) { + const incompatibleNotices: string[] = []; + if (!areApiProposalsCompatible([...extensionManifest.enabledApiProposals], incompatibleNotices)) { + for (const notice of incompatibleNotices) { + validations.push([Severity.Error, notice]); + } + } + } + return validations; } @@ -338,6 +349,34 @@ export function isEngineValid(engine: string, version: string, date: ProductDate return engine === '*' || isVersionValid(version, date, engine); } +export function areApiProposalsCompatible(apiProposals: string[]): boolean; +export function areApiProposalsCompatible(apiProposals: string[], notices: string[]): boolean; +export function areApiProposalsCompatible(apiProposals: string[], productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>): boolean; +export function areApiProposalsCompatible(apiProposals: string[], arg1?: any): boolean { + if (apiProposals.length === 0) { + return true; + } + const notices: string[] | undefined = Array.isArray(arg1) ? arg1 : undefined; + const productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }> = (notices ? undefined : arg1) ?? allApiProposals; + const incompatibleNotices: string[] = []; + const parsedProposals = parseApiProposals(apiProposals); + for (const { proposalName, version } of parsedProposals) { + const existingProposal = productApiProposals[proposalName]; + if (!existingProposal) { + continue; + } + if (!version) { + continue; + } + if (existingProposal.version !== version) { + incompatibleNotices.push(nls.localize('apiProposalMismatch', "Extension is using an API proposal '{0}' that is not compatible with the current version of VS Code.", proposalName)); + } + } + notices?.push(...incompatibleNotices); + return incompatibleNotices.length === 0; + +} + function isVersionValid(currentVersion: string, date: ProductDate, requestedVersion: string, notices: string[] = []): boolean { const desiredVersion = normalizeVersion(parseVersion(requestedVersion)); diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index c20c8fee34b..3a69f8ae76a 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -41,7 +41,7 @@ export interface IDebugger { } export interface IGrammar { - language: string; + language?: string; } export interface IJSONValidation { @@ -239,7 +239,9 @@ export interface IExtensionIdentifier { } export const EXTENSION_CATEGORIES = [ + 'AI', 'Azure', + 'Chat', 'Data Science', 'Debuggers', 'Extension Packs', @@ -256,8 +258,6 @@ export const EXTENSION_CATEGORIES = [ 'Testing', 'Themes', 'Visualization', - 'AI', - 'Chat', 'Other', ]; @@ -497,6 +497,17 @@ export function isResolverExtension(manifest: IExtensionManifest, remoteAuthorit return false; } +export function parseApiProposals(enabledApiProposals: string[]): { proposalName: string; version?: number }[] { + return enabledApiProposals.map(proposal => { + const [proposalName, version] = proposal.split('@'); + return { proposalName, version: version ? parseInt(version) : undefined }; + }); +} + +export function parseEnabledApiProposalNames(enabledApiProposals: string[]): string[] { + return enabledApiProposals.map(proposal => proposal.split('@')[0]); +} + export const IBuiltinExtensionsScannerService = createDecorator('IBuiltinExtensionsScannerService'); export interface IBuiltinExtensionsScannerService { readonly _serviceBrand: undefined; diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts new file mode 100644 index 00000000000..04cc863dffd --- /dev/null +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -0,0 +1,378 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY. + +const _allApiProposals = { + activeComment: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.activeComment.d.ts', + }, + aiRelatedInformation: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', + }, + aiTextSearchProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', + }, + attributableCoverage: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts', + }, + authGetSessions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authGetSessions.d.ts', + }, + authLearnMore: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authLearnMore.d.ts', + }, + authSession: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', + }, + canonicalUriProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts', + }, + chatParticipantAdditions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts', + }, + chatParticipantPrivate: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', + }, + chatProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', + }, + chatTab: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatTab.d.ts', + }, + chatVariableResolver: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts', + }, + codeActionAI: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codeActionAI.d.ts', + }, + codeActionRanges: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codeActionRanges.d.ts', + }, + codiconDecoration: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts', + }, + commentReactor: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReactor.d.ts', + }, + commentReveal: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReveal.d.ts', + }, + commentThreadApplicability: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts', + }, + commentingRangeHint: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentingRangeHint.d.ts', + }, + commentsDraftState: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsDraftState.d.ts', + }, + contribAccessibilityHelpContent: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribAccessibilityHelpContent.d.ts', + }, + contribCommentEditorActionsMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentEditorActionsMenu.d.ts', + }, + contribCommentPeekContext: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts', + }, + contribCommentThreadAdditionalMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts', + }, + contribCommentsViewThreadMenus: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts', + }, + contribDiffEditorGutterToolBarMenus: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribDiffEditorGutterToolBarMenus.d.ts', + }, + contribEditSessions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts', + }, + contribEditorContentMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditorContentMenu.d.ts', + }, + contribIssueReporter: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribIssueReporter.d.ts', + }, + contribLabelFormatterWorkspaceTooltip: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts', + }, + contribMenuBarHome: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMenuBarHome.d.ts', + }, + contribMergeEditorMenus: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMergeEditorMenus.d.ts', + }, + contribMultiDiffEditorMenus: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMultiDiffEditorMenus.d.ts', + }, + contribNotebookStaticPreloads: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribNotebookStaticPreloads.d.ts', + }, + contribRemoteHelp: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribRemoteHelp.d.ts', + }, + contribShareMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribShareMenu.d.ts', + }, + contribSourceControlHistoryItemGroupMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemGroupMenu.d.ts', + }, + contribSourceControlHistoryItemMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemMenu.d.ts', + }, + contribSourceControlInputBoxMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlInputBoxMenu.d.ts', + }, + contribSourceControlTitleMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlTitleMenu.d.ts', + }, + contribStatusBarItems: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribStatusBarItems.d.ts', + }, + contribViewsRemote: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts', + }, + contribViewsWelcome: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts', + }, + createFileSystemWatcher: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts', + }, + customEditorMove: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts', + }, + debugVisualization: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugVisualization.d.ts', + }, + defaultChatParticipant: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts', + }, + diffCommand: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts', + }, + diffContentOptions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffContentOptions.d.ts', + }, + documentFiltersExclusive: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', + }, + documentPaste: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentPaste.d.ts', + }, + editSessionIdentityProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts', + }, + editorHoverVerbosityLevel: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts', + }, + editorInsets: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorInsets.d.ts', + }, + embeddings: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.embeddings.d.ts', + }, + extensionRuntime: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', + }, + extensionsAny: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionsAny.d.ts', + }, + externalUriOpener: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.externalUriOpener.d.ts', + }, + fileComments: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileComments.d.ts', + }, + fileSearchProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts', + }, + findFiles2: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findFiles2.d.ts', + }, + findTextInFiles: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFiles.d.ts', + }, + fsChunks: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fsChunks.d.ts', + }, + idToken: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.idToken.d.ts', + }, + inlineCompletionsAdditions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts', + }, + inlineEdit: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineEdit.d.ts', + }, + interactive: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactive.d.ts', + }, + interactiveWindow: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveWindow.d.ts', + }, + ipc: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', + }, + languageModelSystem: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModelSystem.d.ts', + }, + languageStatusText: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageStatusText.d.ts', + }, + lmTools: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.lmTools.d.ts', + version: 2 + }, + mappedEditsProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts', + }, + multiDocumentHighlightProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts', + }, + newSymbolNamesProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.newSymbolNamesProvider.d.ts', + }, + notebookCellExecution: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecution.d.ts', + }, + notebookCellExecutionState: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', + }, + notebookControllerAffinityHidden: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts', + }, + notebookDeprecated: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts', + }, + notebookExecution: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookExecution.d.ts', + }, + notebookKernelSource: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookKernelSource.d.ts', + }, + notebookLiveShare: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts', + }, + notebookMessaging: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMessaging.d.ts', + }, + notebookMime: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMime.d.ts', + }, + notebookVariableProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts', + }, + portsAttributes: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts', + }, + positronResolveSymlinks: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.positronResolveSymlinks.d.ts', + }, + profileContentHandlers: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.profileContentHandlers.d.ts', + }, + quickDiffProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts', + }, + quickPickItemTooltip: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts', + }, + quickPickSortByLabel: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts', + }, + resolvers: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts', + }, + scmActionButton: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts', + }, + scmHistoryProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts', + }, + scmMultiDiffEditor: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmMultiDiffEditor.d.ts', + }, + scmSelectedProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts', + }, + scmTextDocument: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmTextDocument.d.ts', + }, + scmValidation: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts', + }, + shareProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.shareProvider.d.ts', + }, + showLocal: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.showLocal.d.ts', + }, + speech: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.speech.d.ts', + }, + tabInputMultiDiff: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts', + }, + tabInputTextMerge: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts', + }, + taskPresentationGroup: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts', + }, + telemetry: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.telemetry.d.ts', + }, + terminalDataWriteEvent: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', + }, + terminalDimensions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts', + }, + terminalExecuteCommandEvent: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalExecuteCommandEvent.d.ts', + }, + terminalQuickFixProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts', + }, + terminalSelection: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts', + }, + terminalShellIntegration: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts', + }, + testObserver: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', + }, + textSearchProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', + }, + timeline: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts', + }, + tokenInformation: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tokenInformation.d.ts', + }, + treeViewActiveItem: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts', + }, + treeViewMarkdownMessage: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewMarkdownMessage.d.ts', + }, + treeViewReveal: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewReveal.d.ts', + }, + tunnelFactory: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts', + }, + tunnels: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnels.d.ts', + }, + workspaceTrust: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts', + } +}; +export const allApiProposals = Object.freeze<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>(_allApiProposals); +export type ApiProposalName = keyof typeof _allApiProposals; diff --git a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts index fec3b6eded6..106963ac0b5 100644 --- a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts +++ b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Promises } from 'vs/base/common/async'; import { canceled } from 'vs/base/common/errors'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter'; import { Event } from 'vs/base/common/event'; -import { ILogService } from 'vs/platform/log/common/log'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; -import { Promises } from 'vs/base/common/async'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WindowUtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter { +export class ExtensionHostStarter extends Disposable implements IDisposable, IExtensionHostStarter { readonly _serviceBrand: undefined; @@ -29,16 +29,18 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter @IWindowsMainService private readonly _windowsMainService: IWindowsMainService, @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { + super(); // On shutdown: gracefully await extension host shutdowns - this._lifecycleMainService.onWillShutdown(e => { + this._register(this._lifecycleMainService.onWillShutdown(e => { this._shutdown = true; e.join('extHostStarter', this._waitForAllExit(6000)); - }); + })); } - dispose(): void { + override dispose(): void { // Intentionally not killing the extension host processes + super.dispose(); } private _getExtHost(id: string): WindowUtilityProcess { @@ -72,7 +74,8 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter const id = String(++ExtensionHostStarter._lastId); const extHost = new WindowUtilityProcess(this._logService, this._windowsMainService, this._telemetryService, this._lifecycleMainService); this._extHosts.set(id, extHost); - extHost.onExit(({ pid, code, signal }) => { + const disposable = extHost.onExit(({ pid, code, signal }) => { + disposable.dispose(); this._logService.info(`Extension host with pid ${pid} exited with code: ${code}, signal: ${signal}.`); setTimeout(() => { extHost.dispose(); diff --git a/src/vs/platform/extensions/test/common/extensionValidator.test.ts b/src/vs/platform/extensions/test/common/extensionValidator.test.ts index 9060783778a..6ac5821e08a 100644 --- a/src/vs/platform/extensions/test/common/extensionValidator.test.ts +++ b/src/vs/platform/extensions/test/common/extensionValidator.test.ts @@ -2,10 +2,10 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -import { INormalizedVersion, IParsedVersion, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; +import { areApiProposalsCompatible, INormalizedVersion, IParsedVersion, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; suite('Extension Version Validator', () => { @@ -423,4 +423,21 @@ suite('Extension Version Validator', () => { }; assert.strictEqual(isValidExtensionVersion('1.44.0', undefined, manifest, false, []), false); }); + + test('areApiProposalsCompatible', () => { + assert.strictEqual(areApiProposalsCompatible([]), true); + assert.strictEqual(areApiProposalsCompatible([], ['hello']), true); + assert.strictEqual(areApiProposalsCompatible([], {}), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1'], {}), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal1': { proposal: '' } }), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal1': { proposal: '', version: 1 } }), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '', version: 1 } }), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal2': { proposal: '' } }), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1', 'proposal2'], {}), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1', 'proposal2'], { 'proposal1': { proposal: '' } }), true); + + assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '', version: 2 } }), false); + assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '' } }), false); + }); + }); diff --git a/src/vs/platform/extensions/test/common/extensions.test.ts b/src/vs/platform/extensions/test/common/extensions.test.ts new file mode 100644 index 00000000000..7b81268b347 --- /dev/null +++ b/src/vs/platform/extensions/test/common/extensions.test.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { parseEnabledApiProposalNames } from 'vs/platform/extensions/common/extensions'; + +suite('Parsing Enabled Api Proposals', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('parsingEnabledApiProposals', () => { + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@1'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@randomstring'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@1234'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@1234_random'])); + }); + +}); diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 149665bb571..290a991a35d 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -1470,6 +1470,7 @@ export interface IFilesConfigurationNode { watcherInclude: string[]; encoding: string; autoGuessEncoding: boolean; + candidateGuessEncodings: string[]; defaultLanguage: string; trimTrailingWhitespace: boolean; autoSave: string; diff --git a/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts index 05ff156de89..eef16ccfcb0 100644 --- a/src/vs/platform/files/common/watcher.ts +++ b/src/vs/platform/files/common/watcher.ts @@ -181,16 +181,14 @@ export interface IUniversalWatcher extends IWatcher { export abstract class AbstractWatcherClient extends Disposable { - private static readonly MAX_RESTARTS_PER_REQUEST_ERROR = 3; // how often we give a request a chance to restart on error - private static readonly MAX_RESTARTS_PER_UNKNOWN_ERROR = 10; // how often we give the watcher a chance to restart on unknown errors (like crash) + private static readonly MAX_RESTARTS = 5; private watcher: IWatcher | undefined; private readonly watcherDisposables = this._register(new MutableDisposable()); private requests: IWatchRequest[] | undefined = undefined; - private restartsPerRequestError = new Map(); - private restartsPerUnknownError = 0; + private restartCounter = 0; constructor( private readonly onFileChanges: (changes: IFileChange[]) => void, @@ -224,41 +222,51 @@ export abstract class AbstractWatcherClient extends Disposable { protected onError(error: string, failedRequest?: IUniversalWatchRequest): void { - // Restart on error (up to N times, if enabled) - if (this.options.restartOnError && this.requests?.length) { - - // A request failed - if (failedRequest) { - const restartsPerRequestError = this.restartsPerRequestError.get(failedRequest.path) ?? 0; - if (restartsPerRequestError < AbstractWatcherClient.MAX_RESTARTS_PER_REQUEST_ERROR) { - this.error(`restarting watcher from error in watch request (retrying request): ${error} (${JSON.stringify(failedRequest)})`); - this.restartsPerRequestError.set(failedRequest.path, restartsPerRequestError + 1); - this.restart(this.requests); - } else { - this.error(`restarting watcher from error in watch request (skipping request): ${error} (${JSON.stringify(failedRequest)})`); - this.restart(this.requests.filter(request => request.path !== failedRequest.path)); - } - } - - // Any request failed or process crashed - else { - if (this.restartsPerUnknownError < AbstractWatcherClient.MAX_RESTARTS_PER_UNKNOWN_ERROR) { - this.error(`restarting watcher after unknown global error: ${error}`); - this.restartsPerUnknownError++; - this.restart(this.requests); - } else { - this.error(`giving up attempting to restart watcher after error: ${error}`); - } + // Restart on error (up to N times, if possible) + if (this.canRestart(error, failedRequest)) { + if (this.restartCounter < AbstractWatcherClient.MAX_RESTARTS && this.requests) { + this.error(`restarting watcher after unexpected error: ${error}`); + this.restart(this.requests); + } else { + this.error(`gave up attempting to restart watcher after unexpected error: ${error}`); } } - // Do not attempt to restart if not enabled + // Do not attempt to restart otherwise, report the error else { this.error(error); } } + private canRestart(error: string, failedRequest?: IUniversalWatchRequest): boolean { + if (!this.options.restartOnError) { + return false; // disabled by options + } + + if (failedRequest) { + // do not treat a failing request as a reason to restart the entire + // watcher. it is possible that from a large amount of watch requests + // some fail and we would constantly restart all requests only because + // of that. rather, continue the watcher and leave the failed request + return false; + } + + if ( + error.indexOf('No space left on device') !== -1 || + error.indexOf('EMFILE') !== -1 + ) { + // do not restart when the error indicates that the system is running + // out of handles for file watching. this is not recoverable anyway + // and needs changes to the system before continuing + return false; + } + + return true; + } + private restart(requests: IUniversalWatchRequest[]): void { + this.restartCounter++; + this.init(); this.watch(requests); } diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 180aa7e2960..5e923bdfab5 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import { gracefulify } from 'graceful-fs'; +import { Stats } from 'fs'; import { Barrier, retry } from 'vs/base/common/async'; import { ResourceMap } from 'vs/base/common/map'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -24,22 +23,9 @@ import { readFileIntoStream } from 'vs/platform/files/common/io'; import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, ILogMessage } from 'vs/platform/files/common/watcher'; import { ILogService } from 'vs/platform/log/common/log'; import { AbstractDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/common/diskFileSystemProvider'; -import { toErrorMessage } from 'vs/base/common/errorMessage'; import { UniversalWatcherClient } from 'vs/platform/files/node/watcher/watcherClient'; import { NodeJSWatcherClient } from 'vs/platform/files/node/watcher/nodejs/nodejsClient'; -/** - * Enable graceful-fs very early from here to have it enabled - * in all contexts that leverage the disk file system provider. - */ -(() => { - try { - gracefulify(fs); - } catch (error) { - console.error(`Error enabling graceful-fs: ${toErrorMessage(error)}`); - } -})(); - export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, @@ -139,7 +125,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple } } - private toType(entry: fs.Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType { + private toType(entry: Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType { // Signal file type by checking for file / directory, except: // - symbolic links pointing to nonexistent files are FileType.Unknown diff --git a/src/vs/platform/files/node/watcher/baseWatcher.ts b/src/vs/platform/files/node/watcher/baseWatcher.ts index 3576f20d4f7..c5665396da9 100644 --- a/src/vs/platform/files/node/watcher/baseWatcher.ts +++ b/src/vs/platform/files/node/watcher/baseWatcher.ts @@ -9,7 +9,7 @@ import { ILogMessage, IRecursiveWatcherWithSubscribe, IUniversalWatchRequest, IW import { Emitter, Event } from 'vs/base/common/event'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; -import { DeferredPromise } from 'vs/base/common/async'; +import { DeferredPromise, ThrottledDelayer } from 'vs/base/common/async'; export abstract class BaseWatcher extends Disposable implements IWatcher { @@ -28,6 +28,8 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { private readonly suspendedWatchRequests = this._register(new DisposableMap()); private readonly suspendedWatchRequestsWithPolling = new Set(); + private readonly updateWatchersDelayer = this._register(new ThrottledDelayer(this.getUpdateWatchersDelay())); + protected readonly suspendedWatchRequestPollingInterval: number = 5007; // node.js default private joinWatch = new DeferredPromise(); @@ -88,17 +90,21 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { } } - return await this.updateWatchers(); + return await this.updateWatchers(false /* not delayed */); } finally { this.joinWatch.complete(); } } - private updateWatchers(): Promise { - return this.doWatch([ + private updateWatchers(delayed: boolean): Promise { + return this.updateWatchersDelayer.trigger(() => this.doWatch([ ...this.allNonCorrelatedWatchRequests, ...Array.from(this.allCorrelatedWatchRequests.values()).filter(request => !this.suspendedWatchRequests.has(request.correlationId)) - ]); + ]), delayed ? this.getUpdateWatchersDelay() : 0); + } + + protected getUpdateWatchersDelay(): number { + return 800; } isSuspended(request: IUniversalWatchRequest): 'polling' | boolean { @@ -130,14 +136,14 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { this.monitorSuspendedWatchRequest(request, disposables); - this.updateWatchers(); + this.updateWatchers(true /* delay this call as we might accumulate many failing watch requests on startup */); } private resumeWatchRequest(request: IWatchRequestWithCorrelation): void { this.suspendedWatchRequests.deleteAndDispose(request.correlationId); this.suspendedWatchRequestsWithPolling.delete(request.correlationId); - this.updateWatchers(); + this.updateWatchers(false); } private monitorSuspendedWatchRequest(request: IWatchRequestWithCorrelation, disposables: DisposableStore): void { diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index afabd7aed12..46d213c12e8 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -226,7 +226,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS if (request.pollingInterval) { this.startPolling(request, request.pollingInterval); } else { - this.startWatching(request); + await this.startWatching(request); } } } @@ -322,7 +322,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS pollingWatcher.schedule(0); } - private startWatching(request: IRecursiveWatchRequest, restarts = 0): void { + private async startWatching(request: IRecursiveWatchRequest, restarts = 0): Promise { const cts = new CancellationTokenSource(); const instance = new DeferredPromise(); @@ -349,36 +349,38 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // Path checks for symbolic links / wrong casing const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); - parcelWatcher.subscribe(realPath, (error, parcelEvents) => { - if (watcher.token.isCancellationRequested) { - return; // return early when disposed - } + try { + const parcelWatcherInstance = await parcelWatcher.subscribe(realPath, (error, parcelEvents) => { + if (watcher.token.isCancellationRequested) { + return; // return early when disposed + } - // In any case of an error, treat this like a unhandled exception - // that might require the watcher to restart. We do not really know - // the state of parcel at this point and as such will try to restart - // up to our maximum of restarts. - if (error) { - this.onUnexpectedError(error, request); - } + // In any case of an error, treat this like a unhandled exception + // that might require the watcher to restart. We do not really know + // the state of parcel at this point and as such will try to restart + // up to our maximum of restarts. + if (error) { + this.onUnexpectedError(error, request); + } + + // Handle & emit events + this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength); + }, { + backend: ParcelWatcher.PARCEL_WATCHER_BACKEND, + ignore: watcher.request.excludes + }); - // Handle & emit events - this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength); - }, { - backend: ParcelWatcher.PARCEL_WATCHER_BACKEND, - ignore: watcher.request.excludes - }).then(parcelWatcher => { this.trace(`Started watching: '${realPath}' with backend '${ParcelWatcher.PARCEL_WATCHER_BACKEND}'`); - instance.complete(parcelWatcher); - }).catch(error => { + instance.complete(parcelWatcherInstance); + } catch (error) { this.onUnexpectedError(error, request); instance.complete(undefined); watcher.notifyWatchFailed(); this._onDidWatchFail.fire(request); - }); + } } private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: ParcelWatcherInstance, realPathDiffers: boolean, realPathLength: number): void { @@ -662,7 +664,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS if (watcher.request.pollingInterval) { this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1); } else { - this.startWatching(watcher.request, watcher.restarts + 1); + await this.startWatching(watcher.request, watcher.restarts + 1); } } finally { restartPromise.complete(); diff --git a/src/vs/platform/files/test/browser/fileService.test.ts b/src/vs/platform/files/test/browser/fileService.test.ts index 114e98adefc..166cf549688 100644 --- a/src/vs/platform/files/test/browser/fileService.test.ts +++ b/src/vs/platform/files/test/browser/fileService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DeferredPromise, timeout } from 'vs/base/common/async'; import { bufferToReadable, bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; diff --git a/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts b/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts index 9290520ecda..c5f542918ba 100644 --- a/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts +++ b/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IndexedDB } from 'vs/base/browser/indexedDB'; import { bufferToReadable, bufferToStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/platform/files/test/common/files.test.ts b/src/vs/platform/files/test/common/files.test.ts index 1de7f86398a..245d2222b9d 100644 --- a/src/vs/platform/files/test/common/files.test.ts +++ b/src/vs/platform/files/test/common/files.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isEqual, isEqualOrParent } from 'vs/base/common/extpath'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/files/test/common/watcher.test.ts b/src/vs/platform/files/test/common/watcher.test.ts index 23776f54d82..6d7bb7f3b83 100644 --- a/src/vs/platform/files/test/common/watcher.test.ts +++ b/src/vs/platform/files/test/common/watcher.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { isLinux, isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/platform/files/test/node/diskFileService.integrationTest.ts b/src/vs/platform/files/test/node/diskFileService.integrationTest.ts index fbf1ea0d870..fc2f28ffa97 100644 --- a/src/vs/platform/files/test/node/diskFileService.integrationTest.ts +++ b/src/vs/platform/files/test/node/diskFileService.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { createReadStream, existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs'; import { tmpdir } from 'os'; import { timeout } from 'vs/base/common/async'; diff --git a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts index 836b67ed9d2..77b3946042d 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { tmpdir } from 'os'; import { basename, dirname, join } from 'vs/base/common/path'; import { Promises, RimRafMode } from 'vs/base/node/pfs'; @@ -40,6 +40,10 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int readonly onWatchFail = this._onDidWatchFail.event; + protected override getUpdateWatchersDelay(): number { + return 0; + } + protected override async doWatch(requests: INonRecursiveWatchRequest[]): Promise { await super.doWatch(requests); for (const watcher of this.watchers) { diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index db85e28f341..341edbff2a1 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { realpathSync } from 'fs'; import { tmpdir } from 'os'; import { timeout } from 'vs/base/common/async'; @@ -42,6 +42,10 @@ export class TestParcelWatcher extends ParcelWatcher { return this.removeDuplicateRequests(requests, false /* validate paths skipped for tests */).map(request => request.path); } + protected override getUpdateWatchersDelay(): number { + return 0; + } + protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise { await super.doWatch(requests); await this.whenReady(); diff --git a/src/vs/platform/hover/test/browser/nullHoverService.ts b/src/vs/platform/hover/test/browser/nullHoverService.ts index 6b7b728325b..44c73f8a0a6 100644 --- a/src/vs/platform/hover/test/browser/nullHoverService.ts +++ b/src/vs/platform/hover/test/browser/nullHoverService.ts @@ -10,7 +10,7 @@ export const NullHoverService: IHoverService = { _serviceBrand: undefined, hideHover: () => undefined, showHover: () => undefined, - setupUpdatableHover: () => Disposable.None as any, + setupManagedHover: () => Disposable.None as any, showAndFocusLastHover: () => undefined, - triggerUpdatableHover: () => undefined + showManagedHover: () => undefined }; diff --git a/src/vs/platform/instantiation/test/common/graph.test.ts b/src/vs/platform/instantiation/test/common/graph.test.ts index c5abc22ccff..99e8e0e75a0 100644 --- a/src/vs/platform/instantiation/test/common/graph.test.ts +++ b/src/vs/platform/instantiation/test/common/graph.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Graph } from 'vs/platform/instantiation/common/graph'; diff --git a/src/vs/platform/instantiation/test/common/instantiationService.test.ts b/src/vs/platform/instantiation/test/common/instantiationService.test.ts index fd5bb77d2f7..70718ba712d 100644 --- a/src/vs/platform/instantiation/test/common/instantiationService.test.ts +++ b/src/vs/platform/instantiation/test/common/instantiationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { dispose } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index 81f3a8083ca..856c58c1873 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -127,18 +127,25 @@ export const IIssueMainService = createDecorator('issueServic export interface IIssueMainService { readonly _serviceBrand: undefined; - stopTracing(): Promise; - openReporter(data: IssueReporterData): Promise; - openProcessExplorer(data: ProcessExplorerData): Promise; - getSystemStatus(): Promise; // Used by the issue reporter - - $getSystemInfo(): Promise; - $getPerformanceInfo(): Promise; + openReporter(data: IssueReporterData): Promise; $reloadWithExtensionsDisabled(): Promise; $showConfirmCloseDialog(): Promise; $showClipboardDialog(): Promise; $sendReporterMenu(extensionId: string, extensionName: string): Promise; $closeReporter(): Promise; } + +export const IProcessMainService = createDecorator('processService'); + +export interface IProcessMainService { + readonly _serviceBrand: undefined; + getSystemStatus(): Promise; + stopTracing(): Promise; + openProcessExplorer(data: ProcessExplorerData): Promise; + + // Used by the process explorer + $getSystemInfo(): Promise; + $getPerformanceInfo(): Promise; +} diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index 62e2d9ff341..40f1df2e7be 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -3,35 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, BrowserWindowConstructorOptions, contentTracing, Display, IpcMainEvent, screen } from 'electron'; +import { BrowserWindow, BrowserWindowConstructorOptions, Display, screen } from 'electron'; import { arch, release, type } from 'os'; import { raceTimeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { randomPath } from 'vs/base/common/extpath'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; -import { listProcesses } from 'vs/base/node/ps'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { localize } from 'vs/nls'; -import { IDiagnosticsService, isRemoteDiagnosticError, PerformanceInfo, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { IDiagnosticsMainService } from 'vs/platform/diagnostics/electron-main/diagnosticsMainService'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { IIssueMainService, IssueReporterData, IssueReporterWindowConfiguration, ProcessExplorerData, ProcessExplorerWindowConfiguration } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, IssueReporterData, IssueReporterWindowConfiguration } from 'vs/platform/issue/common/issue'; import { ILogService } from 'vs/platform/log/common/log'; import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; import product from 'vs/platform/product/common/product'; -import { IProductService } from 'vs/platform/product/common/productService'; import { IIPCObjectUrl, IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; -import { IStateService } from 'vs/platform/state/node/state'; -import { UtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; import { zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; import { ICodeWindow, IWindowState } from 'vs/platform/window/electron-main/window'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -const processExplorerWindowState = 'issue.processExplorerWindowState'; - interface IBrowserWindowOptions { backgroundColor: string | undefined; title: string; @@ -50,94 +41,15 @@ export class IssueMainService implements IIssueMainService { private issueReporterWindow: BrowserWindow | null = null; private issueReporterParentWindow: BrowserWindow | null = null; - private processExplorerWindow: BrowserWindow | null = null; - private processExplorerParentWindow: BrowserWindow | null = null; - constructor( private userEnv: IProcessEnvironment, @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILogService private readonly logService: ILogService, - @IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService, - @IDiagnosticsMainService private readonly diagnosticsMainService: IDiagnosticsMainService, @IDialogMainService private readonly dialogMainService: IDialogMainService, @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, @IProtocolMainService private readonly protocolMainService: IProtocolMainService, - @IProductService private readonly productService: IProductService, - @IStateService private readonly stateService: IStateService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - ) { - this.registerListeners(); - } - - //#region Register Listeners - - private registerListeners(): void { - validatedIpcMain.on('vscode:listProcesses', async event => { - const processes = []; - - try { - processes.push({ name: localize('local', "Local"), rootProcess: await listProcesses(process.pid) }); - - const remoteDiagnostics = await this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true }); - remoteDiagnostics.forEach(data => { - if (isRemoteDiagnosticError(data)) { - processes.push({ - name: data.hostName, - rootProcess: data - }); - } else { - if (data.processes) { - processes.push({ - name: data.hostName, - rootProcess: data.processes - }); - } - } - }); - } catch (e) { - this.logService.error(`Listing processes failed: ${e}`); - } - - this.safeSend(event, 'vscode:listProcessesResponse', processes); - }); - - validatedIpcMain.on('vscode:workbenchCommand', (_: unknown, commandInfo: { id: any; from: any; args: any }) => { - const { id, from, args } = commandInfo; - - let parentWindow: BrowserWindow | null; - switch (from) { - case 'processExplorer': - parentWindow = this.processExplorerParentWindow; - break; - default: - // The issue reporter does not use this anymore. - throw new Error(`Unexpected command source: ${from}`); - } - - parentWindow?.webContents.send('vscode:runAction', { id, from, args }); - }); - - validatedIpcMain.on('vscode:closeProcessExplorer', event => { - this.processExplorerWindow?.close(); - }); - - validatedIpcMain.on('vscode:pidToNameRequest', async event => { - const mainProcessInfo = await this.diagnosticsMainService.getMainDiagnostics(); - - const pidToNames: [number, string][] = []; - for (const window of mainProcessInfo.windows) { - pidToNames.push([window.pid, `window [${window.id}] (${window.title})`]); - } - - for (const { pid, name } of UtilityProcess.getAll()) { - pidToNames.push([pid, name]); - } - - this.safeSend(event, 'vscode:pidToNameResponse', pidToNames); - }); - } - - //#endregion + ) { } //#region Used by renderer @@ -196,125 +108,9 @@ export class IssueMainService implements IIssueMainService { } } - async openProcessExplorer(data: ProcessExplorerData): Promise { - if (!this.processExplorerWindow) { - this.processExplorerParentWindow = BrowserWindow.getFocusedWindow(); - if (this.processExplorerParentWindow) { - const processExplorerDisposables = new DisposableStore(); - - const processExplorerWindowConfigUrl = processExplorerDisposables.add(this.protocolMainService.createIPCObjectUrl()); - - const savedPosition = this.stateService.getItem(processExplorerWindowState, undefined); - const position = isStrictWindowState(savedPosition) ? savedPosition : this.getWindowPosition(this.processExplorerParentWindow, 800, 500); - - this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, { - backgroundColor: data.styles.backgroundColor, - title: localize('processExplorer', "Process Explorer"), - zoomLevel: data.zoomLevel, - alwaysOnTop: true - }, 'process-explorer'); - - // Store into config object URL - processExplorerWindowConfigUrl.update({ - appRoot: this.environmentMainService.appRoot, - windowId: this.processExplorerWindow.id, - userEnv: this.userEnv, - data, - product - }); - - this.processExplorerWindow.loadURL( - FileAccess.asBrowserUri(`vs/code/electron-sandbox/processExplorer/processExplorer${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true) - ); - - this.processExplorerWindow.on('close', () => { - this.processExplorerWindow = null; - processExplorerDisposables.dispose(); - }); - - this.processExplorerParentWindow.on('close', () => { - if (this.processExplorerWindow) { - this.processExplorerWindow.close(); - this.processExplorerWindow = null; - - processExplorerDisposables.dispose(); - } - }); - - const storeState = () => { - if (!this.processExplorerWindow) { - return; - } - const size = this.processExplorerWindow.getSize(); - const position = this.processExplorerWindow.getPosition(); - if (!size || !position) { - return; - } - const state: IWindowState = { - width: size[0], - height: size[1], - x: position[0], - y: position[1] - }; - this.stateService.setItem(processExplorerWindowState, state); - }; - - this.processExplorerWindow.on('moved', storeState); - this.processExplorerWindow.on('resized', storeState); - } - } - - if (this.processExplorerWindow) { - this.focusWindow(this.processExplorerWindow); - } - } - - async stopTracing(): Promise { - if (!this.environmentMainService.args.trace) { - return; // requires tracing to be on - } - - const path = await contentTracing.stopRecording(`${randomPath(this.environmentMainService.userHome.fsPath, this.productService.applicationName)}.trace.txt`); - - // Inform user to report an issue - await this.dialogMainService.showMessageBox({ - type: 'info', - message: localize('trace.message', "Successfully created the trace file"), - detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), - buttons: [localize({ key: 'trace.ok', comment: ['&& denotes a mnemonic'] }, "&&OK")], - }, BrowserWindow.getFocusedWindow() ?? undefined); - - // Show item in explorer - this.nativeHostMainService.showItemInFolder(undefined, path); - } - - async getSystemStatus(): Promise { - const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]); - - return this.diagnosticsService.getDiagnostics(info, remoteData); - } - //#endregion //#region used by issue reporter window - - async $getSystemInfo(): Promise { - const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]); - const msg = await this.diagnosticsService.getSystemInfo(info, remoteData); - return msg; - } - - async $getPerformanceInfo(): Promise { - try { - const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true })]); - return await this.diagnosticsService.getPerformanceInfo(info, remoteData); - } catch (error) { - this.logService.warn('issueService#getPerformanceInfo ', error.message); - - throw error; - } - } - async $reloadWithExtensionsDisabled(): Promise { if (this.issueReporterParentWindow) { try { @@ -389,10 +185,6 @@ export class IssueMainService implements IIssueMainService { this.issueReporterWindow?.close(); } - async closeProcessExplorer(): Promise { - this.processExplorerWindow?.close(); - } - //#endregion private focusWindow(window: BrowserWindow): void { @@ -403,12 +195,6 @@ export class IssueMainService implements IIssueMainService { window.focus(); } - private safeSend(event: IpcMainEvent, channel: string, ...args: unknown[]): void { - if (!event.sender.isDestroyed()) { - event.sender.send(channel, ...args); - } - } - private createBrowserWindow(position: IWindowState, ipcObjectUrl: IIPCObjectUrl, options: IBrowserWindowOptions, windowKind: string): BrowserWindow { const window = new BrowserWindow({ fullscreen: false, @@ -509,15 +295,3 @@ export class IssueMainService implements IIssueMainService { return state; } } - -function isStrictWindowState(obj: unknown): obj is IStrictWindowState { - if (typeof obj !== 'object' || obj === null) { - return false; - } - return ( - 'x' in obj && - 'y' in obj && - 'width' in obj && - 'height' in obj - ); -} diff --git a/src/vs/platform/issue/electron-main/processMainService.ts b/src/vs/platform/issue/electron-main/processMainService.ts new file mode 100644 index 00000000000..9e2cc9d0fd3 --- /dev/null +++ b/src/vs/platform/issue/electron-main/processMainService.ts @@ -0,0 +1,375 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BrowserWindow, BrowserWindowConstructorOptions, contentTracing, Display, IpcMainEvent, screen } from 'electron'; +import { randomPath } from 'vs/base/common/extpath'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { FileAccess } from 'vs/base/common/network'; +import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; +import { listProcesses } from 'vs/base/node/ps'; +import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; +import { localize } from 'vs/nls'; +import { IDiagnosticsService, isRemoteDiagnosticError, PerformanceInfo, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { IDiagnosticsMainService } from 'vs/platform/diagnostics/electron-main/diagnosticsMainService'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; +import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { IProcessMainService, ProcessExplorerData, ProcessExplorerWindowConfiguration } from 'vs/platform/issue/common/issue'; +import { ILogService } from 'vs/platform/log/common/log'; +import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; +import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IIPCObjectUrl, IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; +import { IStateService } from 'vs/platform/state/node/state'; +import { UtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; +import { zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; +import { IWindowState } from 'vs/platform/window/electron-main/window'; + +const processExplorerWindowState = 'issue.processExplorerWindowState'; + +interface IBrowserWindowOptions { + backgroundColor: string | undefined; + title: string; + zoomLevel: number; + alwaysOnTop: boolean; +} + +type IStrictWindowState = Required>; + +export class ProcessMainService implements IProcessMainService { + + declare readonly _serviceBrand: undefined; + + private static readonly DEFAULT_BACKGROUND_COLOR = '#1E1E1E'; + + private processExplorerWindow: BrowserWindow | null = null; + private processExplorerParentWindow: BrowserWindow | null = null; + + constructor( + private userEnv: IProcessEnvironment, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, + @ILogService private readonly logService: ILogService, + @IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService, + @IDiagnosticsMainService private readonly diagnosticsMainService: IDiagnosticsMainService, + @IDialogMainService private readonly dialogMainService: IDialogMainService, + @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, + @IProtocolMainService private readonly protocolMainService: IProtocolMainService, + @IProductService private readonly productService: IProductService, + @IStateService private readonly stateService: IStateService, + ) { + this.registerListeners(); + } + + //#region Register Listeners + + private registerListeners(): void { + validatedIpcMain.on('vscode:listProcesses', async event => { + const processes = []; + + try { + processes.push({ name: localize('local', "Local"), rootProcess: await listProcesses(process.pid) }); + + const remoteDiagnostics = await this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true }); + remoteDiagnostics.forEach(data => { + if (isRemoteDiagnosticError(data)) { + processes.push({ + name: data.hostName, + rootProcess: data + }); + } else { + if (data.processes) { + processes.push({ + name: data.hostName, + rootProcess: data.processes + }); + } + } + }); + } catch (e) { + this.logService.error(`Listing processes failed: ${e}`); + } + + this.safeSend(event, 'vscode:listProcessesResponse', processes); + }); + + validatedIpcMain.on('vscode:workbenchCommand', (_: unknown, commandInfo: { id: any; from: any; args: any }) => { + const { id, from, args } = commandInfo; + + let parentWindow: BrowserWindow | null; + switch (from) { + case 'processExplorer': + parentWindow = this.processExplorerParentWindow; + break; + default: + // The issue reporter does not use this anymore. + throw new Error(`Unexpected command source: ${from}`); + } + + parentWindow?.webContents.send('vscode:runAction', { id, from, args }); + }); + + validatedIpcMain.on('vscode:closeProcessExplorer', event => { + this.processExplorerWindow?.close(); + }); + + validatedIpcMain.on('vscode:pidToNameRequest', async event => { + const mainProcessInfo = await this.diagnosticsMainService.getMainDiagnostics(); + + const pidToNames: [number, string][] = []; + for (const window of mainProcessInfo.windows) { + pidToNames.push([window.pid, `window [${window.id}] (${window.title})`]); + } + + for (const { pid, name } of UtilityProcess.getAll()) { + pidToNames.push([pid, name]); + } + + this.safeSend(event, 'vscode:pidToNameResponse', pidToNames); + }); + } + + async openProcessExplorer(data: ProcessExplorerData): Promise { + if (!this.processExplorerWindow) { + this.processExplorerParentWindow = BrowserWindow.getFocusedWindow(); + if (this.processExplorerParentWindow) { + const processExplorerDisposables = new DisposableStore(); + + const processExplorerWindowConfigUrl = processExplorerDisposables.add(this.protocolMainService.createIPCObjectUrl()); + + const savedPosition = this.stateService.getItem(processExplorerWindowState, undefined); + const position = isStrictWindowState(savedPosition) ? savedPosition : this.getWindowPosition(this.processExplorerParentWindow, 800, 500); + + this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, { + backgroundColor: data.styles.backgroundColor, + title: localize('processExplorer', "Process Explorer"), + zoomLevel: data.zoomLevel, + alwaysOnTop: true + }, 'process-explorer'); + + // Store into config object URL + processExplorerWindowConfigUrl.update({ + appRoot: this.environmentMainService.appRoot, + windowId: this.processExplorerWindow.id, + userEnv: this.userEnv, + data, + product + }); + + this.processExplorerWindow.loadURL( + FileAccess.asBrowserUri(`vs/code/electron-sandbox/processExplorer/processExplorer${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true) + ); + + this.processExplorerWindow.on('close', () => { + this.processExplorerWindow = null; + processExplorerDisposables.dispose(); + }); + + this.processExplorerParentWindow.on('close', () => { + if (this.processExplorerWindow) { + this.processExplorerWindow.close(); + this.processExplorerWindow = null; + + processExplorerDisposables.dispose(); + } + }); + + const storeState = () => { + if (!this.processExplorerWindow) { + return; + } + const size = this.processExplorerWindow.getSize(); + const position = this.processExplorerWindow.getPosition(); + if (!size || !position) { + return; + } + const state: IWindowState = { + width: size[0], + height: size[1], + x: position[0], + y: position[1] + }; + this.stateService.setItem(processExplorerWindowState, state); + }; + + this.processExplorerWindow.on('moved', storeState); + this.processExplorerWindow.on('resized', storeState); + } + } + + if (this.processExplorerWindow) { + this.focusWindow(this.processExplorerWindow); + } + } + + private focusWindow(window: BrowserWindow): void { + if (window.isMinimized()) { + window.restore(); + } + + window.focus(); + } + + private getWindowPosition(parentWindow: BrowserWindow, defaultWidth: number, defaultHeight: number): IStrictWindowState { + + // We want the new window to open on the same display that the parent is in + let displayToUse: Display | undefined; + const displays = screen.getAllDisplays(); + + // Single Display + if (displays.length === 1) { + displayToUse = displays[0]; + } + + // Multi Display + else { + + // on mac there is 1 menu per window so we need to use the monitor where the cursor currently is + if (isMacintosh) { + const cursorPoint = screen.getCursorScreenPoint(); + displayToUse = screen.getDisplayNearestPoint(cursorPoint); + } + + // if we have a last active window, use that display for the new window + if (!displayToUse && parentWindow) { + displayToUse = screen.getDisplayMatching(parentWindow.getBounds()); + } + + // fallback to primary display or first display + if (!displayToUse) { + displayToUse = screen.getPrimaryDisplay() || displays[0]; + } + } + + const displayBounds = displayToUse.bounds; + + const state: IStrictWindowState = { + width: defaultWidth, + height: defaultHeight, + x: displayBounds.x + (displayBounds.width / 2) - (defaultWidth / 2), + y: displayBounds.y + (displayBounds.height / 2) - (defaultHeight / 2) + }; + + if (displayBounds.width > 0 && displayBounds.height > 0 /* Linux X11 sessions sometimes report wrong display bounds */) { + if (state.x < displayBounds.x) { + state.x = displayBounds.x; // prevent window from falling out of the screen to the left + } + + if (state.y < displayBounds.y) { + state.y = displayBounds.y; // prevent window from falling out of the screen to the top + } + + if (state.x > (displayBounds.x + displayBounds.width)) { + state.x = displayBounds.x; // prevent window from falling out of the screen to the right + } + + if (state.y > (displayBounds.y + displayBounds.height)) { + state.y = displayBounds.y; // prevent window from falling out of the screen to the bottom + } + + if (state.width > displayBounds.width) { + state.width = displayBounds.width; // prevent window from exceeding display bounds width + } + + if (state.height > displayBounds.height) { + state.height = displayBounds.height; // prevent window from exceeding display bounds height + } + } + + return state; + } + + async stopTracing(): Promise { + if (!this.environmentMainService.args.trace) { + return; // requires tracing to be on + } + + const path = await contentTracing.stopRecording(`${randomPath(this.environmentMainService.userHome.fsPath, this.productService.applicationName)}.trace.txt`); + + // Inform user to report an issue + await this.dialogMainService.showMessageBox({ + type: 'info', + message: localize('trace.message', "Successfully created the trace file"), + detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), + buttons: [localize({ key: 'trace.ok', comment: ['&& denotes a mnemonic'] }, "&&OK")], + }, BrowserWindow.getFocusedWindow() ?? undefined); + + // Show item in explorer + this.nativeHostMainService.showItemInFolder(undefined, path); + } + + async getSystemStatus(): Promise { + const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]); + return this.diagnosticsService.getDiagnostics(info, remoteData); + } + + async $getSystemInfo(): Promise { + const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]); + const msg = await this.diagnosticsService.getSystemInfo(info, remoteData); + return msg; + } + + async $getPerformanceInfo(): Promise { + try { + const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true })]); + return await this.diagnosticsService.getPerformanceInfo(info, remoteData); + } catch (error) { + this.logService.warn('issueService#getPerformanceInfo ', error.message); + + throw error; + } + } + + private createBrowserWindow(position: IWindowState, ipcObjectUrl: IIPCObjectUrl, options: IBrowserWindowOptions, windowKind: string): BrowserWindow { + const window = new BrowserWindow({ + fullscreen: false, + skipTaskbar: false, + resizable: true, + width: position.width, + height: position.height, + minWidth: 300, + minHeight: 200, + x: position.x, + y: position.y, + title: options.title, + backgroundColor: options.backgroundColor || ProcessMainService.DEFAULT_BACKGROUND_COLOR, + webPreferences: { + preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-sandbox/preload.js').fsPath, + additionalArguments: [`--vscode-window-config=${ipcObjectUrl.resource.toString()}`], + v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none', + enableWebSQL: false, + spellcheck: false, + zoomFactor: zoomLevelToZoomFactor(options.zoomLevel), + sandbox: true + }, + alwaysOnTop: options.alwaysOnTop, + experimentalDarkMode: true + } as BrowserWindowConstructorOptions & { experimentalDarkMode: boolean }); + + window.setMenuBarVisibility(false); + + return window; + } + + private safeSend(event: IpcMainEvent, channel: string, ...args: unknown[]): void { + if (!event.sender.isDestroyed()) { + event.sender.send(channel, ...args); + } + } + + async closeProcessExplorer(): Promise { + this.processExplorerWindow?.close(); + } +} + +function isStrictWindowState(obj: unknown): obj is IStrictWindowState { + if (typeof obj !== 'object' || obj === null) { + return false; + } + return ( + 'x' in obj && + 'y' in obj && + 'width' in obj && + 'height' in obj + ); +} diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index 2f79c811d5e..88aa86a05d3 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { createSimpleKeybinding, ResolvedKeybinding, KeyCodeChord, Keybinding } from 'vs/base/common/keybindings'; import { Disposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts index dfc5df1e096..b88a747e454 100644 --- a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { OperatingSystem } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts index 54bd2a6761d..3fccf387540 100644 --- a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { decodeKeybinding, createSimpleKeybinding, KeyCodeChord } from 'vs/base/common/keybindings'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { OS } from 'vs/base/common/platform'; diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 6662122e64f..066d6f7ed1f 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -1494,7 +1494,7 @@ configurationRegistry.registerConfiguration({ type: 'number', minimum: 1, default: 7, - markdownDescription: localize('sticky scroll maximum items', "Controls the number of sticky elements displayed in the tree when `#workbench.tree.enableStickyScroll#` is enabled."), + markdownDescription: localize('sticky scroll maximum items', "Controls the number of sticky elements displayed in the tree when {0} is enabled.", '`#workbench.tree.enableStickyScroll#`'), }, [typeNavigationModeSettingKey]: { type: 'string', diff --git a/src/vs/platform/markers/test/common/markerService.test.ts b/src/vs/platform/markers/test/common/markerService.test.ts index d8ebccf746f..1b9916a8adf 100644 --- a/src/vs/platform/markers/test/common/markerService.test.ts +++ b/src/vs/platform/markers/test/common/markerService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 7e96549a723..d7449e94541 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -25,6 +25,7 @@ import { IUpdateService, StateType } from 'vs/platform/update/common/update'; import { INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, IWindowOpenable, hasNativeTitlebar } from 'vs/platform/window/common/window'; import { IWindowsCountChangedEvent, IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; +import { Disposable } from 'vs/base/common/lifecycle'; const telemetryFrom = 'menu'; @@ -42,7 +43,7 @@ interface IMenuItemWithKeybinding { userSettingsLabel?: string; } -export class Menubar { +export class Menubar extends Disposable { private static readonly lastKnownMenubarStorageKey = 'lastKnownMenubarData'; @@ -78,6 +79,8 @@ export class Menubar { @IProductService private readonly productService: IProductService, @IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService ) { + super(); + this.menuUpdater = new RunOnceScheduler(() => this.doUpdateMenu(), 0); this.menuGC = new RunOnceScheduler(() => { this.oldMenus = []; }, 10000); @@ -169,12 +172,12 @@ export class Menubar { private registerListeners(): void { // Keep flag when app quits - this.lifecycleMainService.onWillShutdown(() => this.willShutdown = true); + this._register(this.lifecycleMainService.onWillShutdown(() => this.willShutdown = true)); // Listen to some events from window service to update menu - this.windowsMainService.onDidChangeWindowsCount(e => this.onDidChangeWindowsCount(e)); - this.nativeHostMainService.onDidBlurMainWindow(() => this.onDidChangeWindowFocus()); - this.nativeHostMainService.onDidFocusMainWindow(() => this.onDidChangeWindowFocus()); + this._register(this.windowsMainService.onDidChangeWindowsCount(e => this.onDidChangeWindowsCount(e))); + this._register(this.nativeHostMainService.onDidBlurMainWindow(() => this.onDidChangeWindowFocus())); + this._register(this.nativeHostMainService.onDidFocusMainWindow(() => this.onDidChangeWindowFocus())); } private get currentEnableMenuBarMnemonics(): boolean { diff --git a/src/vs/platform/menubar/electron-main/menubarMainService.ts b/src/vs/platform/menubar/electron-main/menubarMainService.ts index c680afa296a..731070e4f50 100644 --- a/src/vs/platform/menubar/electron-main/menubarMainService.ts +++ b/src/vs/platform/menubar/electron-main/menubarMainService.ts @@ -8,6 +8,7 @@ import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle import { ILogService } from 'vs/platform/log/common/log'; import { ICommonMenubarService, IMenubarData } from 'vs/platform/menubar/common/menubar'; import { Menubar } from 'vs/platform/menubar/electron-main/menubar'; +import { Disposable } from 'vs/base/common/lifecycle'; export const IMenubarMainService = createDecorator('menubarMainService'); @@ -15,24 +16,24 @@ export interface IMenubarMainService extends ICommonMenubarService { readonly _serviceBrand: undefined; } -export class MenubarMainService implements IMenubarMainService { +export class MenubarMainService extends Disposable implements IMenubarMainService { declare readonly _serviceBrand: undefined; - private menubar: Promise; + private readonly menubar = this.installMenuBarAfterWindowOpen(); constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @ILogService private readonly logService: ILogService ) { - this.menubar = this.installMenuBarAfterWindowOpen(); + super(); } private async installMenuBarAfterWindowOpen(): Promise { await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen); - return this.instantiationService.createInstance(Menubar); + return this._register(this.instantiationService.createInstance(Menubar)); } async updateMenubar(windowId: number, menus: IMenubarData): Promise { diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 645275f1797..300ad57c09a 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -505,7 +505,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async isAdmin(): Promise { let isAdmin: boolean; if (isWindows) { - isAdmin = (await import('native-is-elevated'))(); + isAdmin = (await import('native-is-elevated')).default(); } else { isAdmin = process.getuid?.() === 0; } diff --git a/src/vs/platform/observable/common/platformObservableUtils.ts b/src/vs/platform/observable/common/platformObservableUtils.ts index 096993beb80..2e886ef6540 100644 --- a/src/vs/platform/observable/common/platformObservableUtils.ts +++ b/src/vs/platform/observable/common/platformObservableUtils.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable } from 'vs/base/common/lifecycle'; -import { autorunOpts, IObservable, IReader, observableFromEvent } from 'vs/base/common/observable'; +import { autorunOpts, IObservable, IReader } from 'vs/base/common/observable'; +import { observableFromEventOpts } from 'vs/base/common/observableInternal/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyValue, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; /** Creates an observable update when a configuration key updates. */ export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( + return observableFromEventOpts({ debugName: () => `Configuration Key "${key}"`, }, (handleChange) => configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(key)) { handleChange(e); diff --git a/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts index 710292ef17d..eb93b66a122 100644 --- a/src/vs/platform/opener/browser/link.ts +++ b/src/vs/platform/opener/browser/link.ts @@ -14,7 +14,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import 'vs/css!./link'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; export interface ILinkDescriptor { @@ -33,7 +33,7 @@ export interface ILinkOptions { export class Link extends Disposable { private el: HTMLAnchorElement; - private hover?: IUpdatableHover; + private hover?: IManagedHover; private hoverDelegate: IHoverDelegate; private _enabled: boolean = true; @@ -131,7 +131,7 @@ export class Link extends Disposable { if (this.hoverDelegate.showNativeHover) { this.el.title = title ?? ''; } else if (!this.hover && title) { - this.hover = this._register(this._hoverService.setupUpdatableHover(this.hoverDelegate, this.el, title)); + this.hover = this._register(this._hoverService.setupManagedHover(this.hoverDelegate, this.el, title)); } else if (this.hover) { this.hover.update(title); } diff --git a/src/vs/platform/opener/test/common/opener.test.ts b/src/vs/platform/opener/test/common/opener.test.ts index 35b6e027d66..93ee50f9901 100644 --- a/src/vs/platform/opener/test/common/opener.test.ts +++ b/src/vs/platform/opener/test/common/opener.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { extractSelection, withSelection } from 'vs/platform/opener/common/opener'; diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 15a826b3670..6e55d756c38 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -58,7 +58,7 @@ else { // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.90.0-dev', + version: '1.91.0-dev', // --- Start Positron --- positronVersion: '2024.03.0', nameShort: 'Positron', diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index e1eb0a2f5f2..2a1c7349aae 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -65,7 +65,7 @@ export interface IProgressNotificationOptions extends IProgressOptions { readonly secondaryActions?: readonly IAction[]; readonly delay?: number; readonly priority?: NotificationPriority; - readonly type?: 'syncing' | 'loading'; + readonly type?: 'loading' | 'syncing'; } export interface IProgressDialogOptions extends IProgressOptions { @@ -77,7 +77,7 @@ export interface IProgressDialogOptions extends IProgressOptions { export interface IProgressWindowOptions extends IProgressOptions { readonly location: ProgressLocation.Window; readonly command?: string; - readonly type?: 'syncing' | 'loading'; + readonly type?: 'loading' | 'syncing'; } export interface IProgressCompositeOptions extends IProgressOptions { diff --git a/src/vs/platform/progress/test/common/progress.test.ts b/src/vs/platform/progress/test/common/progress.test.ts index 85bce306781..24c2ddb78de 100644 --- a/src/vs/platform/progress/test/common/progress.test.ts +++ b/src/vs/platform/progress/test/common/progress.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AsyncProgress } from 'vs/platform/progress/common/progress'; diff --git a/src/vs/platform/quickinput/browser/quickInputTree.ts b/src/vs/platform/quickinput/browser/quickInputTree.ts index 6fa28c37170..c65ad7ef4d7 100644 --- a/src/vs/platform/quickinput/browser/quickInputTree.ts +++ b/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -36,7 +36,7 @@ import { ltrim } from 'vs/base/common/strings'; import { RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree'; import { ThrottledDelayer } from 'vs/base/common/async'; import { isCancellationError } from 'vs/base/common/errors'; -import type { IHoverWidget, IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import type { IHoverWidget, IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { QuickPickFocus } from '../common/quickInput'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -434,7 +434,7 @@ class QuickPickItemElementRenderer extends BaseQuickInputListRenderer boolean; - readonly includeSymbols?: boolean; /** * @deprecated - temporary for Dynamic Chat Variables (see usage) until it has built-in UX for file picking * Useful for adding items to the top of the list that might contain actions. diff --git a/src/vs/platform/quickinput/test/browser/quickinput.test.ts b/src/vs/platform/quickinput/test/browser/quickinput.test.ts index 8e8fc694be0..328c43d72fb 100644 --- a/src/vs/platform/quickinput/test/browser/quickinput.test.ts +++ b/src/vs/platform/quickinput/test/browser/quickinput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { unthemedInboxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { unthemedButtonStyles } from 'vs/base/browser/ui/button/button'; import { unthemedListStyles } from 'vs/base/browser/ui/list/listWidget'; @@ -57,7 +57,7 @@ suite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 setup(() => { const fixture = document.createElement('div'); mainWindow.document.body.appendChild(fixture); - store.add(toDisposable(() => mainWindow.document.body.removeChild(fixture))); + store.add(toDisposable(() => fixture.remove())); const instantiationService = new TestInstantiationService(); diff --git a/src/vs/platform/registry/test/common/platform.test.ts b/src/vs/platform/registry/test/common/platform.test.ts index 3fe9188fa8d..8ec965d503b 100644 --- a/src/vs/platform/registry/test/common/platform.test.ts +++ b/src/vs/platform/registry/test/common/platform.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isFunction } from 'vs/base/common/types'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/src/vs/platform/remote/common/remoteExtensionsScanner.ts b/src/vs/platform/remote/common/remoteExtensionsScanner.ts index 792c0352bb7..a26de9d171b 100644 --- a/src/vs/platform/remote/common/remoteExtensionsScanner.ts +++ b/src/vs/platform/remote/common/remoteExtensionsScanner.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -16,5 +15,4 @@ export interface IRemoteExtensionsScannerService { whenExtensionsReady(): Promise; scanExtensions(): Promise; - scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise; } diff --git a/src/vs/platform/remote/test/common/remoteHosts.test.ts b/src/vs/platform/remote/test/common/remoteHosts.test.ts index cfbc105f465..ed564551df9 100644 --- a/src/vs/platform/remote/test/common/remoteHosts.test.ts +++ b/src/vs/platform/remote/test/common/remoteHosts.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { parseAuthorityWithOptionalPort, parseAuthorityWithPort } from 'vs/platform/remote/common/remoteHosts'; diff --git a/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts b/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts index a5bacbb8c86..67790a807c6 100644 --- a/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts +++ b/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import product from 'vs/platform/product/common/product'; import { IProductService } from 'vs/platform/product/common/productService'; diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 289ad6740e0..06265628b94 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -155,6 +155,12 @@ function registerProxyConfigurations(scope: ConfigurationScope): void { markdownDescription: localize('proxyKerberosServicePrincipal', "Overrides the principal service name for Kerberos authentication with the HTTP proxy. A default based on the proxy hostname is used when this is not set."), restricted: true }, + 'http.noProxy': { + type: 'array', + items: { type: 'string' }, + markdownDescription: localize('noProxy', "Specifies domain names for which proxy settings should be ignored for HTTP/HTTPS requests."), + restricted: true + }, 'http.proxyAuthorization': { type: ['null', 'string'], default: null, diff --git a/src/vs/platform/secrets/test/common/secrets.test.ts b/src/vs/platform/secrets/test/common/secrets.test.ts index b3a048af2f2..50def3a9a92 100644 --- a/src/vs/platform/secrets/test/common/secrets.test.ts +++ b/src/vs/platform/secrets/test/common/secrets.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEncryptionService, KnownStorageProvider } from 'vs/platform/encryption/common/encryptionService'; diff --git a/src/vs/platform/state/test/node/state.test.ts b/src/vs/platform/state/test/node/state.test.ts index 493d78d0e51..acce49d3e9c 100644 --- a/src/vs/platform/state/test/node/state.test.ts +++ b/src/vs/platform/state/test/node/state.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { readFileSync } from 'fs'; import { tmpdir } from 'os'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts b/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts index 2ee6f9bc99b..33a22c391b2 100644 --- a/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts +++ b/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type { ITelemetryItem, ITelemetryUnloadState } from '@microsoft/1ds-core-js'; -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OneDataSystemWebAppender } from 'vs/platform/telemetry/browser/1dsAppender'; import { IAppInsightsCore } from 'vs/platform/telemetry/common/1dsAppender'; diff --git a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts index 8524e9a6996..bc75667e949 100644 --- a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; -import * as sinonTest from 'sinon-test'; +import sinonTest from 'sinon-test'; import { mainWindow } from 'vs/base/browser/window'; import * as Errors from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts b/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts index 8de29c1eeeb..2fe9b7a6477 100644 --- a/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts +++ b/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 426de4a8ff7..5cd3d9be955 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -import type { IPromptInputModel } from 'vs/platform/terminal/common/capabilities/commandDetection/promptInputModel'; +import type { IPromptInputModel, ISerializedPromptInputModel } from 'vs/platform/terminal/common/capabilities/commandDetection/promptInputModel'; import { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand'; import { ITerminalOutputMatch, ITerminalOutputMatcher } from 'vs/platform/terminal/common/terminal'; import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess'; @@ -169,11 +169,6 @@ export interface ICommandDetectionCapability { readonly executingCommandObject: ITerminalCommand | undefined; /** The current cwd at the cursor's position. */ readonly cwd: string | undefined; - /** - * Whether a command is currently being input. If the a command is current not being input or - * the state cannot reliably be detected the fallback of undefined will be used. - */ - readonly hasInput: boolean | undefined; readonly currentCommand: ICurrentPartialCommand | undefined; readonly onCommandStarted: Event; readonly onCommandFinished: Event; @@ -306,6 +301,7 @@ export interface IMarkProperties { export interface ISerializedCommandDetectionCapability { isWindowsPty: boolean; commands: ISerializedTerminalCommand[]; + promptInputModel: ISerializedPromptInputModel | undefined; } export interface IPtyHostProcessReplayEvent { events: ReplayEntry[]; diff --git a/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts index 0f1c53efded..dbce8f54458 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts @@ -49,6 +49,14 @@ export interface IPromptInputModelState { readonly ghostTextIndex: number; } +export interface ISerializedPromptInputModel { + readonly modelState: IPromptInputModelState; + readonly commandStartX: number; + readonly lastPromptLine: string | undefined; + readonly continuationPrompt: string | undefined; + readonly lastUserInput: string; +} + export class PromptInputModel extends Disposable implements IPromptInputModel { private _state: PromptInputState = PromptInputState.Unknown; @@ -142,6 +150,26 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { return result; } + serialize(): ISerializedPromptInputModel { + return { + modelState: this._createStateObject(), + commandStartX: this._commandStartX, + lastPromptLine: this._lastPromptLine, + continuationPrompt: this._continuationPrompt, + lastUserInput: this._lastUserInput + }; + } + + deserialize(serialized: ISerializedPromptInputModel): void { + this._value = serialized.modelState.value; + this._cursorIndex = serialized.modelState.cursorIndex; + this._ghostTextIndex = serialized.modelState.ghostTextIndex; + this._commandStartX = serialized.commandStartX; + this._lastPromptLine = serialized.lastPromptLine; + this._continuationPrompt = serialized.continuationPrompt; + this._lastUserInput = serialized.lastUserInput; + } + private _handleCommandStart(command: { marker: IMarker }) { if (this._state === PromptInputState.Input) { return; @@ -220,8 +248,13 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { const absoluteCursorY = buffer.baseY + buffer.cursorY; let value = commandLine; - let cursorIndex = absoluteCursorY === commandStartY ? this._getRelativeCursorIndex(this._commandStartX, buffer, line) : commandLine.trimEnd().length + 1; let ghostTextIndex = -1; + let cursorIndex: number; + if (absoluteCursorY === commandStartY) { + cursorIndex = this._getRelativeCursorIndex(this._commandStartX, buffer, line); + } else { + cursorIndex = commandLine.trimEnd().length; + } // Detect ghost text by looking for italic or dim text in or after the cursor and // non-italic/dim text in the cell closest non-whitespace cell before the cursor @@ -235,15 +268,25 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { line = buffer.getLine(y); const lineText = line?.translateToString(true); if (lineText && line) { + // Check if the line wrapped without a new line (continuation) + if (line.isWrapped) { + value += lineText; + const relativeCursorIndex = this._getRelativeCursorIndex(0, buffer, line); + if (absoluteCursorY === y) { + cursorIndex += relativeCursorIndex; + } else { + cursorIndex += lineText.length; + } + } // Verify continuation prompt if we have it, if this line doesn't have it then the - // user likely just pressed enter - if (this._continuationPrompt === undefined || this._lineContainsContinuationPrompt(lineText)) { + // user likely just pressed enter. + else if (this._continuationPrompt === undefined || this._lineContainsContinuationPrompt(lineText)) { const trimmedLineText = this._trimContinuationPrompt(lineText); value += `\n${trimmedLineText}`; if (absoluteCursorY === y) { const continuationCellWidth = this._getContinuationPromptCellWidth(line, lineText); const relativeCursorIndex = this._getRelativeCursorIndex(continuationCellWidth, buffer, line); - cursorIndex += relativeCursorIndex; + cursorIndex += relativeCursorIndex + 1; } else { cursorIndex += trimmedLineText.length + 1; } diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 8df3d3b8f42..bef121fbff5 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -56,23 +56,6 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe } get cwd(): string | undefined { return this._cwd; } get promptTerminator(): string | undefined { return this._promptTerminator; } - private get _isInputting(): boolean { - return !!(this._currentCommand.commandStartMarker && !this._currentCommand.commandExecutedMarker); - } - - get hasInput(): boolean | undefined { - if (!this._isInputting || !this._currentCommand?.commandStartMarker) { - return undefined; - } - if (this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY === this._currentCommand.commandStartMarker?.line) { - const line = this._terminal.buffer.active.getLine(this._terminal.buffer.active.cursorY)?.translateToString(true, this._currentCommand.commandStartX); - if (line === undefined) { - return undefined; - } - return line.length > 0; - } - return true; - } private readonly _onCommandStarted = this._register(new Emitter()); readonly onCommandStarted = this._onCommandStarted.event; @@ -425,7 +408,8 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe } return { isWindowsPty: this._ptyHeuristics.value instanceof WindowsPtyHeuristics, - commands + commands, + promptInputModel: this._promptInputModel.serialize(), }; } @@ -460,6 +444,9 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand); this._onCommandFinished.fire(newCommand); } + if (serialized.promptInputModel) { + this._promptInputModel.deserialize(serialized.promptInputModel); + } } } diff --git a/src/vs/platform/terminal/common/terminalRecorder.ts b/src/vs/platform/terminal/common/terminalRecorder.ts index 79a828cc220..417527a976f 100644 --- a/src/vs/platform/terminal/common/terminalRecorder.ts +++ b/src/vs/platform/terminal/common/terminalRecorder.ts @@ -91,7 +91,8 @@ export class TerminalRecorder { // No command restoration is needed when relaunching terminals commands: { isWindowsPty: false, - commands: [] + commands: [], + promptInputModel: undefined, } }; } diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index cacc170e5cd..d335e2c27cd 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -577,7 +577,8 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati if (!this._terminal || !this.capabilities.has(TerminalCapability.CommandDetection)) { return { isWindowsPty: false, - commands: [] + commands: [], + promptInputModel: undefined, }; } const result = this._createOrGetCommandDetection(this._terminal).serialize(); diff --git a/src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts b/src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts index 8c74c72b9c9..ebd0331692e 100644 --- a/src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts +++ b/src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts @@ -38,7 +38,7 @@ export class ElectronPtyHostStarter extends Disposable implements IPtyHostStarte ) { super(); - this._lifecycleMainService.onWillShutdown(() => this._onWillShutdown.fire()); + this._register(this._lifecycleMainService.onWillShutdown(() => this._onWillShutdown.fire())); // Listen for new windows to establish connection directly to pty host validatedIpcMain.on('vscode:createPtyHostMessageChannel', (e, nonce) => this._onWindowConnection(e, nonce)); this._register(toDisposable(() => { diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index f91b87baed4..402dcc7b723 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -108,14 +108,16 @@ export class PtyHostService extends Disposable implements IPtyHostService { this._register(toDisposable(() => this._disposePtyHost())); this._resolveVariablesRequestStore = this._register(new RequestStore(undefined, this._logService)); - this._resolveVariablesRequestStore.onCreateRequest(this._onPtyHostRequestResolveVariables.fire, this._onPtyHostRequestResolveVariables); + this._register(this._resolveVariablesRequestStore.onCreateRequest(this._onPtyHostRequestResolveVariables.fire, this._onPtyHostRequestResolveVariables)); // Start the pty host when a window requests a connection, if the starter has that capability. if (this._ptyHostStarter.onRequestConnection) { - Event.once(this._ptyHostStarter.onRequestConnection)(() => this._ensurePtyHost()); + this._register(Event.once(this._ptyHostStarter.onRequestConnection)(() => this._ensurePtyHost())); } - this._ptyHostStarter.onWillShutdown?.(() => this._wasQuitRequested = true); + if (this._ptyHostStarter.onWillShutdown) { + this._register(this._ptyHostStarter.onWillShutdown(() => this._wasQuitRequested = true)); + } } private get _ignoreProcessNames(): string[] { diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index 623516d5887..66f1d0b39ef 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -161,6 +161,7 @@ export function getShellIntegrationInjection( } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot, ''); + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; if (options.shellIntegration.suggestEnabled) { envMixin['VSCODE_SUGGEST'] = '1'; } @@ -178,6 +179,7 @@ export function getShellIntegrationInjection( } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot); + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; return { newArgs, envMixin }; } logService.warn(`Shell integration cannot be enabled for executable "${shellLaunchConfig.executable}" and args`, shellLaunchConfig.args); @@ -199,6 +201,7 @@ export function getShellIntegrationInjection( } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot); + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; return { newArgs, envMixin }; } case 'fish': { @@ -225,6 +228,7 @@ export function getShellIntegrationInjection( } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot, ''); + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; return { newArgs, envMixin }; } case 'zsh': { diff --git a/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts b/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts index 4566eb4b008..18f410a147f 100644 --- a/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts +++ b/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts @@ -3,8 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// HACK: Ignore warnings, technically this requires browser/ but it's run on renderer.html anyway so +// it's fine in tests. Importing @xterm/headless appears to prevent `yarn test-browser` from running +// at all. // eslint-disable-next-line local/code-import-patterns, local/code-amd-node-module -import { Terminal } from '@xterm/headless'; +import { Terminal } from '@xterm/xterm'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; @@ -469,6 +472,26 @@ suite('PromptInputModel', () => { }); }); + suite('wrapped line (non-continuation)', () => { + test('basic wrapped line', async () => { + xterm.resize(5, 10); + + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('ech'); + await assertPromptInput(`ech|`); + + await writePromise('o '); + await assertPromptInput(`echo |`); + + await writePromise('"a"'); + // HACK: Trailing whitespace is due to flaky detection in wrapped lines (but it doesn't matter much) + await assertPromptInput(`echo "a"| `); + }); + }); + // To "record a session" for these tests: // - Enable debug logging // - Open and clear Terminal output channel diff --git a/src/vs/platform/terminal/test/common/terminalRecorder.test.ts b/src/vs/platform/terminal/test/common/terminalRecorder.test.ts index b1c523a2228..66b317c468b 100644 --- a/src/vs/platform/terminal/test/common/terminalRecorder.test.ts +++ b/src/vs/platform/terminal/test/common/terminalRecorder.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess'; import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; diff --git a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts index 0fc97abb282..5ef7ee9ce94 100644 --- a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts @@ -201,7 +201,8 @@ suite('platform - terminalEnvironment', () => { // --- Start Positron --- POSITRON: '1', // --- End Positron --- - VSCODE_INJECTION: '1' + VSCODE_INJECTION: '1', + VSCODE_STABLE: '0' } }); deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: [] }, enabledProcessOptions, defaultEnvironment, logService, productService), enabledExpectedResult); @@ -219,7 +220,8 @@ suite('platform - terminalEnvironment', () => { POSITRON: '1', // --- End Positron --- VSCODE_INJECTION: '1', - VSCODE_SHELL_LOGIN: '1' + VSCODE_SHELL_LOGIN: '1', + VSCODE_STABLE: '0' } }); test('when array', () => { diff --git a/src/vs/platform/theme/common/colorUtils.ts b/src/vs/platform/theme/common/colorUtils.ts index 2388e7cb702..14ceea8847b 100644 --- a/src/vs/platform/theme/common/colorUtils.ts +++ b/src/vs/platform/theme/common/colorUtils.ts @@ -7,10 +7,11 @@ import { assertNever } from 'vs/base/common/assert'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; -import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; +import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import * as platform from 'vs/platform/registry/common/platform'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; +import * as nls from 'vs/nls'; // ------ API types @@ -19,7 +20,7 @@ export type ColorIdentifier = string; export interface ColorContribution { readonly id: ColorIdentifier; readonly description: string; - readonly defaults: ColorDefaults | null; + readonly defaults: ColorDefaults | ColorValue | null; readonly needsTransparency: boolean; readonly deprecationMessage: string | undefined; } @@ -68,6 +69,9 @@ export interface ColorDefaults { hcLight: ColorValue | null; } +export function isColorDefaults(value: unknown): value is ColorDefaults { + return value !== null && typeof value === 'object' && 'light' in value && 'dark' in value; +} /** * A Color Value is either a color literal, a reference to an other color or a derived color @@ -79,6 +83,8 @@ export const Extensions = { ColorContribution: 'base.contributions.colors' }; +export const DEFAULT_COLOR_CONFIG_VALUE = 'default'; + export interface IColorRegistry { readonly onDidChangeSchema: Event; @@ -117,33 +123,56 @@ export interface IColorRegistry { */ getColorReferenceSchema(): IJSONSchema; + /** + * Notify when the color theme or settings change. + */ + notifyThemeUpdate(theme: IColorTheme): void; + } +type IJSONSchemaForColors = IJSONSchema & { properties: { [name: string]: { oneOf: [IJSONSchemaWithSnippets, IJSONSchema] } } }; +type IJSONSchemaWithSnippets = IJSONSchema & { defaultSnippets: IJSONSchemaSnippet[] }; + class ColorRegistry implements IColorRegistry { private readonly _onDidChangeSchema = new Emitter(); readonly onDidChangeSchema: Event = this._onDidChangeSchema.event; private colorsById: { [key: string]: ColorContribution }; - private colorSchema: IJSONSchema & { properties: IJSONSchemaMap } = { type: 'object', properties: {} }; + private colorSchema: IJSONSchemaForColors = { type: 'object', properties: {} }; private colorReferenceSchema: IJSONSchema & { enum: string[]; enumDescriptions: string[] } = { type: 'string', enum: [], enumDescriptions: [] }; constructor() { this.colorsById = {}; } - public registerColor(id: string, defaults: ColorDefaults | null, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier { + public notifyThemeUpdate(colorThemeData: IColorTheme) { + for (const key of Object.keys(this.colorsById)) { + const color = colorThemeData.getColor(key); + if (color) { + this.colorSchema.properties[key].oneOf[0].defaultSnippets[0].body = `\${1:${color.toString()}}`; + } + } + this._onDidChangeSchema.fire(); + } + + public registerColor(id: string, defaults: ColorDefaults | ColorValue | null, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier { const colorContribution: ColorContribution = { id, description, defaults, needsTransparency, deprecationMessage }; this.colorsById[id] = colorContribution; - const propertySchema: IJSONSchema = { type: 'string', description, format: 'color-hex', defaultSnippets: [{ body: '${1:#ff0000}' }] }; + const propertySchema: IJSONSchemaWithSnippets = { type: 'string', description, format: 'color-hex', defaultSnippets: [{ body: '${1:#ff0000}' }] }; if (deprecationMessage) { propertySchema.deprecationMessage = deprecationMessage; } if (needsTransparency) { propertySchema.pattern = '^#(?:(?[0-9a-fA-f]{3}[0-9a-eA-E])|(?:[0-9a-fA-F]{6}(?:(?![fF]{2})(?:[0-9a-fA-F]{2}))))?$'; - propertySchema.patternErrorMessage = 'This color must be transparent or it will obscure content'; + propertySchema.patternErrorMessage = nls.localize('transparecyRequired', 'This color must be transparent or it will obscure content'); } - this.colorSchema.properties[id] = propertySchema; + this.colorSchema.properties[id] = { + oneOf: [ + propertySchema, + { type: 'string', const: DEFAULT_COLOR_CONFIG_VALUE, description: nls.localize('useDefault', 'Use the default color.') } + ] + }; this.colorReferenceSchema.enum.push(id); this.colorReferenceSchema.enumDescriptions.push(description); @@ -169,8 +198,8 @@ class ColorRegistry implements IColorRegistry { public resolveDefaultColor(id: ColorIdentifier, theme: IColorTheme): Color | undefined { const colorDesc = this.colorsById[id]; - if (colorDesc && colorDesc.defaults) { - const colorValue = colorDesc.defaults[theme.type]; + if (colorDesc?.defaults) { + const colorValue = isColorDefaults(colorDesc.defaults) ? colorDesc.defaults[theme.type] : colorDesc.defaults; return resolveColorValue(colorValue, theme); } return undefined; @@ -203,7 +232,7 @@ const colorRegistry = new ColorRegistry(); platform.Registry.add(Extensions.ColorContribution, colorRegistry); -export function registerColor(id: string, defaults: ColorDefaults | null, description: string, needsTransparency?: boolean, deprecationMessage?: string): ColorIdentifier { +export function registerColor(id: string, defaults: ColorDefaults | ColorValue | null, description: string, needsTransparency?: boolean, deprecationMessage?: string): ColorIdentifier { return colorRegistry.registerColor(id, defaults, description, needsTransparency, deprecationMessage); } @@ -319,6 +348,7 @@ const schemaRegistry = platform.Registry.as(JSONExten schemaRegistry.registerSchema(workbenchColorsSchemaId, colorRegistry.getColorSchema()); const delayer = new RunOnceScheduler(() => schemaRegistry.notifySchemaChanged(workbenchColorsSchemaId), 200); + colorRegistry.onDidChangeSchema(() => { if (!delayer.isScheduled()) { delayer.schedule(); diff --git a/src/vs/platform/theme/common/colors/baseColors.ts b/src/vs/platform/theme/common/colors/baseColors.ts index 9a02e218db8..baf6b86f27f 100644 --- a/src/vs/platform/theme/common/colors/baseColors.ts +++ b/src/vs/platform/theme/common/colors/baseColors.ts @@ -43,7 +43,7 @@ export const activeContrastBorder = registerColor('contrastActiveBorder', nls.localize('activeContrastBorder', "An extra border around active elements to separate them from others for greater contrast.")); export const selectionBackground = registerColor('selection.background', - { light: null, dark: null, hcDark: null, hcLight: null }, + null, nls.localize('selectionBackground', "The background color of text selections in the workbench (e.g. for input fields or text areas). Note that this does not apply to selections within the editor.")); @@ -65,7 +65,7 @@ export const textSeparatorForeground = registerColor('textSeparator.foreground', // ------ text preformat export const textPreformatForeground = registerColor('textPreformat.foreground', - { light: '#A31515', dark: '#D7BA7D', hcDark: '#FFFFFF', hcLight: '#000000' }, + { light: '#A31515', dark: '#D7BA7D', hcDark: '#000000', hcLight: '#FFFFFF' }, nls.localize('textPreformatForeground', "Foreground color for preformatted text segments.")); export const textPreformatBackground = registerColor('textPreformat.background', diff --git a/src/vs/platform/theme/common/colors/chartsColors.ts b/src/vs/platform/theme/common/colors/chartsColors.ts index eb63b602234..a35e296d2ad 100644 --- a/src/vs/platform/theme/common/colors/chartsColors.ts +++ b/src/vs/platform/theme/common/colors/chartsColors.ts @@ -12,27 +12,27 @@ import { minimapFindMatch } from 'vs/platform/theme/common/colors/minimapColors' export const chartsForeground = registerColor('charts.foreground', - { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, + foreground, nls.localize('chartsForeground', "The foreground color used in charts.")); export const chartsLines = registerColor('charts.lines', - { dark: transparent(foreground, .5), light: transparent(foreground, .5), hcDark: transparent(foreground, .5), hcLight: transparent(foreground, .5) }, + transparent(foreground, .5), nls.localize('chartsLines', "The color used for horizontal lines in charts.")); export const chartsRed = registerColor('charts.red', - { dark: editorErrorForeground, light: editorErrorForeground, hcDark: editorErrorForeground, hcLight: editorErrorForeground }, + editorErrorForeground, nls.localize('chartsRed', "The red color used in chart visualizations.")); export const chartsBlue = registerColor('charts.blue', - { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoForeground, hcLight: editorInfoForeground }, + editorInfoForeground, nls.localize('chartsBlue', "The blue color used in chart visualizations.")); export const chartsYellow = registerColor('charts.yellow', - { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningForeground, hcLight: editorWarningForeground }, + editorWarningForeground, nls.localize('chartsYellow', "The yellow color used in chart visualizations.")); export const chartsOrange = registerColor('charts.orange', - { dark: minimapFindMatch, light: minimapFindMatch, hcDark: minimapFindMatch, hcLight: minimapFindMatch }, + minimapFindMatch, nls.localize('chartsOrange', "The orange color used in chart visualizations.")); export const chartsGreen = registerColor('charts.green', diff --git a/src/vs/platform/theme/common/colors/editorColors.ts b/src/vs/platform/theme/common/colors/editorColors.ts index a57b85e2c29..cac6dea162c 100644 --- a/src/vs/platform/theme/common/colors/editorColors.ts +++ b/src/vs/platform/theme/common/colors/editorColors.ts @@ -26,7 +26,7 @@ export const editorForeground = registerColor('editor.foreground', export const editorStickyScrollBackground = registerColor('editorStickyScroll.background', - { light: editorBackground, dark: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, + editorBackground, nls.localize('editorStickyScrollBackground', "Background color of sticky scroll in the editor")); export const editorStickyScrollHoverBackground = registerColor('editorStickyScrollHover.background', @@ -38,7 +38,7 @@ export const editorStickyScrollBorder = registerColor('editorStickyScroll.border nls.localize('editorStickyScrollBorder', "Border color of sticky scroll in the editor")); export const editorStickyScrollShadow = registerColor('editorStickyScroll.shadow', - { dark: scrollbarShadow, light: scrollbarShadow, hcDark: scrollbarShadow, hcLight: scrollbarShadow }, + scrollbarShadow, nls.localize('editorStickyScrollShadow', " Shadow color of sticky scroll in the editor")); @@ -47,7 +47,7 @@ export const editorWidgetBackground = registerColor('editorWidget.background', nls.localize('editorWidgetBackground', 'Background color of editor widgets, such as find/replace.')); export const editorWidgetForeground = registerColor('editorWidget.foreground', - { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, + foreground, nls.localize('editorWidgetForeground', 'Foreground color of editor widgets, such as find/replace.')); export const editorWidgetBorder = registerColor('editorWidget.border', @@ -55,12 +55,12 @@ export const editorWidgetBorder = registerColor('editorWidget.border', nls.localize('editorWidgetBorder', 'Border color of editor widgets. The color is only used if the widget chooses to have a border and if the color is not overridden by a widget.')); export const editorWidgetResizeBorder = registerColor('editorWidget.resizeBorder', - { light: null, dark: null, hcDark: null, hcLight: null }, + null, nls.localize('editorWidgetResizeBorder', "Border color of the resize bar of editor widgets. The color is only used if the widget chooses to have a resize border and if the color is not overridden by a widget.")); export const editorErrorBackground = registerColor('editorError.background', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('editorError.background', 'Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorErrorForeground = registerColor('editorError.foreground', @@ -73,7 +73,7 @@ export const editorErrorBorder = registerColor('editorError.border', export const editorWarningBackground = registerColor('editorWarning.background', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('editorWarning.background', 'Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorWarningForeground = registerColor('editorWarning.foreground', @@ -86,7 +86,7 @@ export const editorWarningBorder = registerColor('editorWarning.border', export const editorInfoBackground = registerColor('editorInfo.background', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('editorInfo.background', 'Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorInfoForeground = registerColor('editorInfo.foreground', @@ -142,7 +142,7 @@ export const editorFindMatch = registerColor('editor.findMatchBackground', nls.localize('editorFindMatch', "Color of the current search match.")); export const editorFindMatchForeground = registerColor('editor.findMatchForeground', - { light: null, dark: null, hcDark: null, hcLight: null }, + null, nls.localize('editorFindMatchForeground', "Text color of the current search match.")); export const editorFindMatchHighlight = registerColor('editor.findMatchHighlightBackground', @@ -150,7 +150,7 @@ export const editorFindMatchHighlight = registerColor('editor.findMatchHighlight nls.localize('findMatchHighlight', "Color of the other search matches. The color must not be opaque so as not to hide underlying decorations."), true); export const editorFindMatchHighlightForeground = registerColor('editor.findMatchHighlightForeground', - { light: null, dark: null, hcDark: null, hcLight: null }, + null, nls.localize('findMatchHighlightForeground', "Foreground color of the other search matches."), true); export const editorFindRangeHighlight = registerColor('editor.findRangeHighlightBackground', @@ -177,15 +177,15 @@ export const editorHoverHighlight = registerColor('editor.hoverHighlightBackgrou nls.localize('hoverHighlight', 'Highlight below the word for which a hover is shown. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorHoverBackground = registerColor('editorHoverWidget.background', - { light: editorWidgetBackground, dark: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + editorWidgetBackground, nls.localize('hoverBackground', 'Background color of the editor hover.')); export const editorHoverForeground = registerColor('editorHoverWidget.foreground', - { light: editorWidgetForeground, dark: editorWidgetForeground, hcDark: editorWidgetForeground, hcLight: editorWidgetForeground }, + editorWidgetForeground, nls.localize('hoverForeground', 'Foreground color of the editor hover.')); export const editorHoverBorder = registerColor('editorHoverWidget.border', - { light: editorWidgetBorder, dark: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, + editorWidgetBorder, nls.localize('hoverBorder', 'Border color of the editor hover.')); export const editorHoverStatusBarBackground = registerColor('editorHoverWidget.statusBarBackground', @@ -204,19 +204,19 @@ export const editorInlayHintBackground = registerColor('editorInlayHint.backgrou nls.localize('editorInlayHintBackground', 'Background color of inline hints')); export const editorInlayHintTypeForeground = registerColor('editorInlayHint.typeForeground', - { dark: editorInlayHintForeground, light: editorInlayHintForeground, hcDark: editorInlayHintForeground, hcLight: editorInlayHintForeground }, + editorInlayHintForeground, nls.localize('editorInlayHintForegroundTypes', 'Foreground color of inline hints for types')); export const editorInlayHintTypeBackground = registerColor('editorInlayHint.typeBackground', - { dark: editorInlayHintBackground, light: editorInlayHintBackground, hcDark: editorInlayHintBackground, hcLight: editorInlayHintBackground }, + editorInlayHintBackground, nls.localize('editorInlayHintBackgroundTypes', 'Background color of inline hints for types')); export const editorInlayHintParameterForeground = registerColor('editorInlayHint.parameterForeground', - { dark: editorInlayHintForeground, light: editorInlayHintForeground, hcDark: editorInlayHintForeground, hcLight: editorInlayHintForeground }, + editorInlayHintForeground, nls.localize('editorInlayHintForegroundParameter', 'Foreground color of inline hints for parameters')); export const editorInlayHintParameterBackground = registerColor('editorInlayHint.parameterBackground', - { dark: editorInlayHintBackground, light: editorInlayHintBackground, hcDark: editorInlayHintBackground, hcLight: editorInlayHintBackground }, + editorInlayHintBackground, nls.localize('editorInlayHintBackgroundParameter', 'Background color of inline hints for parameters')); @@ -231,7 +231,7 @@ export const editorLightBulbAutoFixForeground = registerColor('editorLightBulbAu nls.localize('editorLightBulbAutoFixForeground', "The color used for the lightbulb auto fix actions icon.")); export const editorLightBulbAiForeground = registerColor('editorLightBulbAi.foreground', - { dark: editorLightBulbForeground, light: editorLightBulbForeground, hcDark: editorLightBulbForeground, hcLight: editorLightBulbForeground }, + editorLightBulbForeground, nls.localize('editorLightBulbAiForeground', "The color used for the lightbulb AI icon.")); @@ -242,11 +242,11 @@ export const snippetTabstopHighlightBackground = registerColor('editor.snippetTa nls.localize('snippetTabstopHighlightBackground', "Highlight background color of a snippet tabstop.")); export const snippetTabstopHighlightBorder = registerColor('editor.snippetTabstopHighlightBorder', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('snippetTabstopHighlightBorder', "Highlight border color of a snippet tabstop.")); export const snippetFinalTabstopHighlightBackground = registerColor('editor.snippetFinalTabstopHighlightBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('snippetFinalTabstopHighlightBackground', "Highlight background color of the final tabstop of a snippet.")); export const snippetFinalTabstopHighlightBorder = registerColor('editor.snippetFinalTabstopHighlightBorder', @@ -278,20 +278,20 @@ export const diffRemovedLine = registerColor('diffEditor.removedLineBackground', export const diffInsertedLineGutter = registerColor('diffEditorGutter.insertedLineBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('diffEditorInsertedLineGutter', 'Background color for the margin where lines got inserted.')); export const diffRemovedLineGutter = registerColor('diffEditorGutter.removedLineBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('diffEditorRemovedLineGutter', 'Background color for the margin where lines got removed.')); export const diffOverviewRulerInserted = registerColor('diffEditorOverview.insertedForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('diffEditorOverviewInserted', 'Diff overview ruler foreground for inserted content.')); export const diffOverviewRulerRemoved = registerColor('diffEditorOverview.removedForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('diffEditorOverviewRemoved', 'Diff overview ruler foreground for removed content.')); @@ -314,11 +314,11 @@ export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', export const diffUnchangedRegionBackground = registerColor('diffEditor.unchangedRegionBackground', - { dark: 'sideBar.background', light: 'sideBar.background', hcDark: 'sideBar.background', hcLight: 'sideBar.background' }, + 'sideBar.background', nls.localize('diffEditor.unchangedRegionBackground', "The background color of unchanged blocks in the diff editor.")); export const diffUnchangedRegionForeground = registerColor('diffEditor.unchangedRegionForeground', - { dark: 'foreground', light: 'foreground', hcDark: 'foreground', hcLight: 'foreground' }, + 'foreground', nls.localize('diffEditor.unchangedRegionForeground', "The foreground color of unchanged blocks in the diff editor.")); export const diffUnchangedTextBackground = registerColor('diffEditor.unchangedCodeBackground', @@ -355,11 +355,11 @@ export const toolbarActiveBackground = registerColor('toolbar.activeBackground', // ----- breadcumbs export const breadcrumbsForeground = registerColor('breadcrumb.foreground', - { light: transparent(foreground, 0.8), dark: transparent(foreground, 0.8), hcDark: transparent(foreground, 0.8), hcLight: transparent(foreground, 0.8) }, + transparent(foreground, 0.8), nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); export const breadcrumbsBackground = registerColor('breadcrumb.background', - { light: editorBackground, dark: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, + editorBackground, nls.localize('breadcrumbsBackground', "Background color of breadcrumb items.")); export const breadcrumbsFocusForeground = registerColor('breadcrumb.focusForeground', @@ -371,7 +371,7 @@ export const breadcrumbsActiveSelectionForeground = registerColor('breadcrumb.ac nls.localize('breadcrumbsSelectedForeground', "Color of selected breadcrumb items.")); export const breadcrumbsPickerBackground = registerColor('breadcrumbPicker.background', - { light: editorWidgetBackground, dark: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + editorWidgetBackground, nls.localize('breadcrumbsSelectedBackground', "Background color of breadcrumb item picker.")); @@ -389,7 +389,7 @@ export const mergeCurrentHeaderBackground = registerColor('merge.currentHeaderBa nls.localize('mergeCurrentHeaderBackground', 'Current header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeCurrentContentBackground = registerColor('merge.currentContentBackground', - { dark: transparent(mergeCurrentHeaderBackground, contentTransparency), light: transparent(mergeCurrentHeaderBackground, contentTransparency), hcDark: transparent(mergeCurrentHeaderBackground, contentTransparency), hcLight: transparent(mergeCurrentHeaderBackground, contentTransparency) }, + transparent(mergeCurrentHeaderBackground, contentTransparency), nls.localize('mergeCurrentContentBackground', 'Current content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeIncomingHeaderBackground = registerColor('merge.incomingHeaderBackground', @@ -397,7 +397,7 @@ export const mergeIncomingHeaderBackground = registerColor('merge.incomingHeader nls.localize('mergeIncomingHeaderBackground', 'Incoming header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeIncomingContentBackground = registerColor('merge.incomingContentBackground', - { dark: transparent(mergeIncomingHeaderBackground, contentTransparency), light: transparent(mergeIncomingHeaderBackground, contentTransparency), hcDark: transparent(mergeIncomingHeaderBackground, contentTransparency), hcLight: transparent(mergeIncomingHeaderBackground, contentTransparency) }, + transparent(mergeIncomingHeaderBackground, contentTransparency), nls.localize('mergeIncomingContentBackground', 'Incoming content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeCommonHeaderBackground = registerColor('merge.commonHeaderBackground', @@ -405,7 +405,7 @@ export const mergeCommonHeaderBackground = registerColor('merge.commonHeaderBack nls.localize('mergeCommonHeaderBackground', 'Common ancestor header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeCommonContentBackground = registerColor('merge.commonContentBackground', - { dark: transparent(mergeCommonHeaderBackground, contentTransparency), light: transparent(mergeCommonHeaderBackground, contentTransparency), hcDark: transparent(mergeCommonHeaderBackground, contentTransparency), hcLight: transparent(mergeCommonHeaderBackground, contentTransparency) }, + transparent(mergeCommonHeaderBackground, contentTransparency), nls.localize('mergeCommonContentBackground', 'Common ancestor content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeBorder = registerColor('merge.border', @@ -430,20 +430,20 @@ export const overviewRulerFindMatchForeground = registerColor('editorOverviewRul nls.localize('overviewRulerFindMatchForeground', 'Overview ruler marker color for find matches. The color must not be opaque so as not to hide underlying decorations.'), true); export const overviewRulerSelectionHighlightForeground = registerColor('editorOverviewRuler.selectionHighlightForeground', - { dark: '#A0A0A0CC', light: '#A0A0A0CC', hcDark: '#A0A0A0CC', hcLight: '#A0A0A0CC' }, + '#A0A0A0CC', nls.localize('overviewRulerSelectionHighlightForeground', 'Overview ruler marker color for selection highlights. The color must not be opaque so as not to hide underlying decorations.'), true); // ----- problems export const problemsErrorIconForeground = registerColor('problemsErrorIcon.foreground', - { dark: editorErrorForeground, light: editorErrorForeground, hcDark: editorErrorForeground, hcLight: editorErrorForeground }, + editorErrorForeground, nls.localize('problemsErrorIconForeground', "The color used for the problems error icon.")); export const problemsWarningIconForeground = registerColor('problemsWarningIcon.foreground', - { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningForeground, hcLight: editorWarningForeground }, + editorWarningForeground, nls.localize('problemsWarningIconForeground', "The color used for the problems warning icon.")); export const problemsInfoIconForeground = registerColor('problemsInfoIcon.foreground', - { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoForeground, hcLight: editorInfoForeground }, + editorInfoForeground, nls.localize('problemsInfoIconForeground', "The color used for the problems info icon.")); diff --git a/src/vs/platform/theme/common/colors/inputColors.ts b/src/vs/platform/theme/common/colors/inputColors.ts index dc38222d402..c79c1d2840b 100644 --- a/src/vs/platform/theme/common/colors/inputColors.ts +++ b/src/vs/platform/theme/common/colors/inputColors.ts @@ -21,7 +21,7 @@ export const inputBackground = registerColor('input.background', nls.localize('inputBoxBackground', "Input box background.")); export const inputForeground = registerColor('input.foreground', - { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, + foreground, nls.localize('inputBoxForeground', "Input box foreground.")); export const inputBorder = registerColor('input.border', @@ -110,11 +110,11 @@ export const selectBorder = registerColor('dropdown.border', // ------ button export const buttonForeground = registerColor('button.foreground', - { dark: Color.white, light: Color.white, hcDark: Color.white, hcLight: Color.white }, + Color.white, nls.localize('buttonForeground', "Button foreground color.")); export const buttonSeparator = registerColor('button.separator', - { dark: transparent(buttonForeground, .4), light: transparent(buttonForeground, .4), hcDark: transparent(buttonForeground, .4), hcLight: transparent(buttonForeground, .4) }, + transparent(buttonForeground, .4), nls.localize('buttonSeparator', "Button separator color.")); export const buttonBackground = registerColor('button.background', @@ -126,7 +126,7 @@ export const buttonHoverBackground = registerColor('button.hoverBackground', nls.localize('buttonHoverBackground', "Button background color when hovering.")); export const buttonBorder = registerColor('button.border', - { dark: contrastBorder, light: contrastBorder, hcDark: contrastBorder, hcLight: contrastBorder }, + contrastBorder, nls.localize('buttonBorder', "Button border color.")); export const buttonSecondaryForeground = registerColor('button.secondaryForeground', @@ -145,23 +145,23 @@ export const buttonSecondaryHoverBackground = registerColor('button.secondaryHov // ------ checkbox export const checkboxBackground = registerColor('checkbox.background', - { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, + selectBackground, nls.localize('checkbox.background', "Background color of checkbox widget.")); export const checkboxSelectBackground = registerColor('checkbox.selectBackground', - { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + editorWidgetBackground, nls.localize('checkbox.select.background', "Background color of checkbox widget when the element it's in is selected.")); export const checkboxForeground = registerColor('checkbox.foreground', - { dark: selectForeground, light: selectForeground, hcDark: selectForeground, hcLight: selectForeground }, + selectForeground, nls.localize('checkbox.foreground', "Foreground color of checkbox widget.")); export const checkboxBorder = registerColor('checkbox.border', - { dark: selectBorder, light: selectBorder, hcDark: selectBorder, hcLight: selectBorder }, + selectBorder, nls.localize('checkbox.border', "Border color of checkbox widget.")); export const checkboxSelectBorder = registerColor('checkbox.selectBorder', - { dark: iconForeground, light: iconForeground, hcDark: iconForeground, hcLight: iconForeground }, + iconForeground, nls.localize('checkbox.select.border', "Border color of checkbox widget when the element it's in is selected.")); diff --git a/src/vs/platform/theme/common/colors/listColors.ts b/src/vs/platform/theme/common/colors/listColors.ts index b6f51e3696b..dd5c405199c 100644 --- a/src/vs/platform/theme/common/colors/listColors.ts +++ b/src/vs/platform/theme/common/colors/listColors.ts @@ -15,11 +15,11 @@ import { editorWidgetBackground, editorFindMatchHighlightBorder, editorFindMatch export const listFocusBackground = registerColor('list.focusBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusForeground = registerColor('list.focusForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listFocusForeground', "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusOutline = registerColor('list.focusOutline', @@ -27,7 +27,7 @@ export const listFocusOutline = registerColor('list.focusOutline', nls.localize('listFocusOutline', "List/Tree outline color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusAndSelectionOutline = registerColor('list.focusAndSelectionOutline', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listFocusAndSelectionOutline', "List/Tree outline color for the focused item when the list/tree is active and selected. An active list/tree has keyboard focus, an inactive does not.")); export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', @@ -39,7 +39,7 @@ export const listActiveSelectionForeground = registerColor('list.activeSelection nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listActiveSelectionIconForeground = registerColor('list.activeSelectionIconForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listActiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', @@ -47,19 +47,19 @@ export const listInactiveSelectionBackground = registerColor('list.inactiveSelec nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionIconForeground = registerColor('list.inactiveSelectionIconForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listInactiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listInactiveFocusBackground', "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveFocusOutline = registerColor('list.inactiveFocusOutline', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listInactiveFocusOutline', "List/Tree outline color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listHoverBackground = registerColor('list.hoverBackground', @@ -67,7 +67,7 @@ export const listHoverBackground = registerColor('list.hoverBackground', nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); export const listHoverForeground = registerColor('list.hoverForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); export const listDropOverBackground = registerColor('list.dropBackground', @@ -109,7 +109,7 @@ export const listFilterWidgetNoMatchesOutline = registerColor('listFilterWidget. nls.localize('listFilterWidgetNoMatchesOutline', 'Outline color of the type filter widget in lists and trees, when there are no matches.')); export const listFilterWidgetShadow = registerColor('listFilterWidget.shadow', - { dark: widgetShadow, light: widgetShadow, hcDark: widgetShadow, hcLight: widgetShadow }, + widgetShadow, nls.localize('listFilterWidgetShadow', 'Shadow color of the type filter widget in lists and trees.')); export const listFilterMatchHighlight = registerColor('list.filterMatchBackground', @@ -132,7 +132,7 @@ export const treeIndentGuidesStroke = registerColor('tree.indentGuidesStroke', nls.localize('treeIndentGuidesStroke', "Tree stroke color for the indentation guides.")); export const treeInactiveIndentGuidesStroke = registerColor('tree.inactiveIndentGuidesStroke', - { dark: transparent(treeIndentGuidesStroke, 0.4), light: transparent(treeIndentGuidesStroke, 0.4), hcDark: transparent(treeIndentGuidesStroke, 0.4), hcLight: transparent(treeIndentGuidesStroke, 0.4) }, + transparent(treeIndentGuidesStroke, 0.4), nls.localize('treeInactiveIndentGuidesStroke', "Tree stroke color for the indentation guides that are not active.")); diff --git a/src/vs/platform/theme/common/colors/menuColors.ts b/src/vs/platform/theme/common/colors/menuColors.ts index 6fa9a0ec326..05bf5491952 100644 --- a/src/vs/platform/theme/common/colors/menuColors.ts +++ b/src/vs/platform/theme/common/colors/menuColors.ts @@ -19,19 +19,19 @@ export const menuBorder = registerColor('menu.border', nls.localize('menuBorder', "Border color of menus.")); export const menuForeground = registerColor('menu.foreground', - { dark: selectForeground, light: selectForeground, hcDark: selectForeground, hcLight: selectForeground }, + selectForeground, nls.localize('menuForeground', "Foreground color of menu items.")); export const menuBackground = registerColor('menu.background', - { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, + selectBackground, nls.localize('menuBackground', "Background color of menu items.")); export const menuSelectionForeground = registerColor('menu.selectionForeground', - { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hcDark: listActiveSelectionForeground, hcLight: listActiveSelectionForeground }, + listActiveSelectionForeground, nls.localize('menuSelectionForeground', "Foreground color of the selected menu item in menus.")); export const menuSelectionBackground = registerColor('menu.selectionBackground', - { dark: listActiveSelectionBackground, light: listActiveSelectionBackground, hcDark: listActiveSelectionBackground, hcLight: listActiveSelectionBackground }, + listActiveSelectionBackground, nls.localize('menuSelectionBackground', "Background color of the selected menu item in menus.")); export const menuSelectionBorder = registerColor('menu.selectionBorder', diff --git a/src/vs/platform/theme/common/colors/minimapColors.ts b/src/vs/platform/theme/common/colors/minimapColors.ts index 0b051994d09..ade38578c28 100644 --- a/src/vs/platform/theme/common/colors/minimapColors.ts +++ b/src/vs/platform/theme/common/colors/minimapColors.ts @@ -39,21 +39,21 @@ export const minimapError = registerColor('minimap.errorHighlight', nls.localize('minimapError', 'Minimap marker color for errors.')); export const minimapBackground = registerColor('minimap.background', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('minimapBackground', "Minimap background color.")); export const minimapForegroundOpacity = registerColor('minimap.foregroundOpacity', - { dark: Color.fromHex('#000f'), light: Color.fromHex('#000f'), hcDark: Color.fromHex('#000f'), hcLight: Color.fromHex('#000f') }, + Color.fromHex('#000f'), nls.localize('minimapForegroundOpacity', 'Opacity of foreground elements rendered in the minimap. For example, "#000000c0" will render the elements with 75% opacity.')); export const minimapSliderBackground = registerColor('minimapSlider.background', - { light: transparent(scrollbarSliderBackground, 0.5), dark: transparent(scrollbarSliderBackground, 0.5), hcDark: transparent(scrollbarSliderBackground, 0.5), hcLight: transparent(scrollbarSliderBackground, 0.5) }, + transparent(scrollbarSliderBackground, 0.5), nls.localize('minimapSliderBackground', "Minimap slider background color.")); export const minimapSliderHoverBackground = registerColor('minimapSlider.hoverBackground', - { light: transparent(scrollbarSliderHoverBackground, 0.5), dark: transparent(scrollbarSliderHoverBackground, 0.5), hcDark: transparent(scrollbarSliderHoverBackground, 0.5), hcLight: transparent(scrollbarSliderHoverBackground, 0.5) }, + transparent(scrollbarSliderHoverBackground, 0.5), nls.localize('minimapSliderHoverBackground', "Minimap slider background color when hovering.")); export const minimapSliderActiveBackground = registerColor('minimapSlider.activeBackground', - { light: transparent(scrollbarSliderActiveBackground, 0.5), dark: transparent(scrollbarSliderActiveBackground, 0.5), hcDark: transparent(scrollbarSliderActiveBackground, 0.5), hcLight: transparent(scrollbarSliderActiveBackground, 0.5) }, + transparent(scrollbarSliderActiveBackground, 0.5), nls.localize('minimapSliderActiveBackground', "Minimap slider background color when clicked on.")); diff --git a/src/vs/platform/theme/common/colors/miscColors.ts b/src/vs/platform/theme/common/colors/miscColors.ts index 5a2ea49b702..42a00e23e6a 100644 --- a/src/vs/platform/theme/common/colors/miscColors.ts +++ b/src/vs/platform/theme/common/colors/miscColors.ts @@ -16,7 +16,7 @@ import { contrastBorder, focusBorder } from 'vs/platform/theme/common/colors/bas // ----- sash export const sashHoverBorder = registerColor('sash.hoverBorder', - { dark: focusBorder, light: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, + focusBorder, nls.localize('sashActiveBorder', "Border color of active sashes.")); diff --git a/src/vs/platform/theme/common/colors/quickpickColors.ts b/src/vs/platform/theme/common/colors/quickpickColors.ts index 7f8fc271a6e..3b109a21872 100644 --- a/src/vs/platform/theme/common/colors/quickpickColors.ts +++ b/src/vs/platform/theme/common/colors/quickpickColors.ts @@ -15,11 +15,11 @@ import { listActiveSelectionBackground, listActiveSelectionForeground, listActiv export const quickInputBackground = registerColor('quickInput.background', - { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + editorWidgetBackground, nls.localize('pickerBackground', "Quick picker background color. The quick picker widget is the container for pickers like the command palette.")); export const quickInputForeground = registerColor('quickInput.foreground', - { dark: editorWidgetForeground, light: editorWidgetForeground, hcDark: editorWidgetForeground, hcLight: editorWidgetForeground }, + editorWidgetForeground, nls.localize('pickerForeground', "Quick picker foreground color. The quick picker widget is the container for pickers like the command palette.")); export const quickInputTitleBackground = registerColor('quickInputTitle.background', @@ -35,15 +35,15 @@ export const pickerGroupBorder = registerColor('pickerGroup.border', nls.localize('pickerGroupBorder', "Quick picker color for grouping borders.")); export const _deprecatedQuickInputListFocusBackground = registerColor('quickInput.list.focusBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, '', undefined, + null, '', undefined, nls.localize('quickInput.list.focusBackground deprecation', "Please use quickInputList.focusBackground instead")); export const quickInputListFocusForeground = registerColor('quickInputList.focusForeground', - { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hcDark: listActiveSelectionForeground, hcLight: listActiveSelectionForeground }, + listActiveSelectionForeground, nls.localize('quickInput.listFocusForeground', "Quick picker foreground color for the focused item.")); export const quickInputListFocusIconForeground = registerColor('quickInputList.focusIconForeground', - { dark: listActiveSelectionIconForeground, light: listActiveSelectionIconForeground, hcDark: listActiveSelectionIconForeground, hcLight: listActiveSelectionIconForeground }, + listActiveSelectionIconForeground, nls.localize('quickInput.listFocusIconForeground', "Quick picker icon foreground color for the focused item.")); export const quickInputListFocusBackground = registerColor('quickInputList.focusBackground', diff --git a/src/vs/platform/tunnel/common/tunnel.ts b/src/vs/platform/tunnel/common/tunnel.ts index 86b4da4b409..b1433f2e745 100644 --- a/src/vs/platform/tunnel/common/tunnel.ts +++ b/src/vs/platform/tunnel/common/tunnel.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -215,7 +215,7 @@ export class DisposableTunnel { } } -export abstract class AbstractTunnelService implements ITunnelService { +export abstract class AbstractTunnelService extends Disposable implements ITunnelService { declare readonly _serviceBrand: undefined; private _onTunnelOpened: Emitter = new Emitter(); @@ -234,7 +234,7 @@ export abstract class AbstractTunnelService implements ITunnelService { public constructor( @ILogService protected readonly logService: ILogService, @IConfigurationService protected readonly configurationService: IConfigurationService - ) { } + ) { super(); } get hasTunnelProvider(): boolean { return !!this._tunnelProvider; @@ -308,7 +308,8 @@ export abstract class AbstractTunnelService implements ITunnelService { return tunnels; } - async dispose(): Promise { + override async dispose(): Promise { + super.dispose(); for (const portMap of this._tunnels.values()) { for (const { value } of portMap.values()) { await value.then(tunnel => typeof tunnel !== 'string' ? tunnel?.dispose() : undefined); diff --git a/src/vs/platform/tunnel/test/common/tunnel.test.ts b/src/vs/platform/tunnel/test/common/tunnel.test.ts index d86d3f47bd7..ae32707eb30 100644 --- a/src/vs/platform/tunnel/test/common/tunnel.test.ts +++ b/src/vs/platform/tunnel/test/common/tunnel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { extractLocalHostUriMetaDataForPortMapping, diff --git a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts index 27d69e4e5ac..32d33a7d23c 100644 --- a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts +++ b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts b/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts index 32dde9e11d7..339e86bf938 100644 --- a/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts +++ b/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { mock } from 'vs/base/test/common/mock'; import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; diff --git a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts index b6a05ce558f..8f0c5641d9f 100644 --- a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts +++ b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index b65c078f83a..f18ae097050 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -168,6 +168,10 @@ export type UserDataProfilesObject = { emptyWindows: Map; }; +type TransientUserDataProfilesObject = UserDataProfilesObject & { + folders: ResourceMap; +}; + export type StoredUserDataProfile = { name: string; location: URI; @@ -209,8 +213,9 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf private profileCreationPromises = new Map>(); - protected readonly transientProfilesObject: UserDataProfilesObject = { + protected readonly transientProfilesObject: TransientUserDataProfilesObject = { profiles: [], + folders: new ResourceMap(), workspaces: new ResourceMap(), emptyWindows: new Map() }; @@ -454,6 +459,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } async resetWorkspaces(): Promise { + this.transientProfilesObject.folders.clear(); this.transientProfilesObject.workspaces.clear(); this.transientProfilesObject.emptyWindows.clear(); this.profilesObject.workspaces.clear(); @@ -484,7 +490,17 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf getProfileForWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier): IUserDataProfile | undefined { const workspace = this.getWorkspace(workspaceIdentifier); - return URI.isUri(workspace) ? this.transientProfilesObject.workspaces.get(workspace) ?? this.profilesObject.workspaces.get(workspace) : this.transientProfilesObject.emptyWindows.get(workspace) ?? this.profilesObject.emptyWindows.get(workspace); + const profile = URI.isUri(workspace) ? this.profilesObject.workspaces.get(workspace) : this.profilesObject.emptyWindows.get(workspace); + if (profile) { + return profile; + } + if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { + return this.transientProfilesObject.folders.get(workspaceIdentifier.uri); + } + if (isWorkspaceIdentifier(workspaceIdentifier)) { + return this.transientProfilesObject.workspaces.get(workspaceIdentifier.configPath); + } + return this.transientProfilesObject.emptyWindows.get(workspaceIdentifier.id); } protected getWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier): URI | string { @@ -498,16 +514,19 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } private isProfileAssociatedToWorkspace(profile: IUserDataProfile): boolean { - if ([...this.transientProfilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) { + if ([...this.profilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) { return true; } - if ([...this.transientProfilesObject.workspaces.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { + if ([...this.profilesObject.workspaces.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { return true; } - if ([...this.profilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) { + if ([...this.transientProfilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) { return true; } - if ([...this.profilesObject.workspaces.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { + if ([...this.transientProfilesObject.workspaces.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { + return true; + } + if ([...this.transientProfilesObject.folders.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { return true; } return false; @@ -516,6 +535,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf private updateProfiles(added: IUserDataProfile[], removed: IUserDataProfile[], updated: IUserDataProfile[]): void { const allProfiles = [...this.profiles, ...added]; const storedProfiles: StoredUserDataProfile[] = []; + const transientProfiles = this.transientProfilesObject.profiles; this.transientProfilesObject.profiles = []; for (let profile of allProfiles) { if (profile.isDefault) { @@ -525,9 +545,30 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf continue; } profile = updated.find(p => profile.id === p.id) ?? profile; + const transientProfile = transientProfiles.find(p => profile.id === p.id); if (profile.isTransient) { this.transientProfilesObject.profiles.push(profile); } else { + if (transientProfile) { + for (const [windowId, p] of this.transientProfilesObject.emptyWindows.entries()) { + if (profile.id === p.id) { + this.updateWorkspaceAssociation({ id: windowId }, profile); + break; + } + } + for (const [workspace, p] of this.transientProfilesObject.workspaces.entries()) { + if (profile.id === p.id) { + this.updateWorkspaceAssociation({ id: '', configPath: workspace }, profile); + break; + } + } + for (const [folder, p] of this.transientProfilesObject.folders.entries()) { + if (profile.id === p.id) { + this.updateWorkspaceAssociation({ id: '', uri: folder }, profile); + break; + } + } + } storedProfiles.push({ location: profile.location, name: profile.name, shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); } } @@ -544,30 +585,48 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf // Force transient if the new profile to associate is transient transient = newProfile?.isTransient ? true : transient; - if (!transient) { - // Unset the transiet workspace association if any - this.updateWorkspaceAssociation(workspaceIdentifier, undefined, true); - } + if (transient) { + if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { + this.transientProfilesObject.folders.delete(workspaceIdentifier.uri); + if (newProfile) { + this.transientProfilesObject.folders.set(workspaceIdentifier.uri, newProfile); + } + } - const workspace = this.getWorkspace(workspaceIdentifier); - const profilesObject = transient ? this.transientProfilesObject : this.profilesObject; + else if (isWorkspaceIdentifier(workspaceIdentifier)) { + this.transientProfilesObject.workspaces.delete(workspaceIdentifier.configPath); + if (newProfile) { + this.transientProfilesObject.workspaces.set(workspaceIdentifier.configPath, newProfile); + } + } - // Folder or Multiroot workspace - if (URI.isUri(workspace)) { - profilesObject.workspaces.delete(workspace); - if (newProfile) { - profilesObject.workspaces.set(workspace, newProfile); + else { + this.transientProfilesObject.emptyWindows.delete(workspaceIdentifier.id); + if (newProfile) { + this.transientProfilesObject.emptyWindows.set(workspaceIdentifier.id, newProfile); + } } } - // Empty Window + else { - profilesObject.emptyWindows.delete(workspace); - if (newProfile) { - profilesObject.emptyWindows.set(workspace, newProfile); - } - } + // Unset the transiet workspace association if any + this.updateWorkspaceAssociation(workspaceIdentifier, undefined, true); + const workspace = this.getWorkspace(workspaceIdentifier); - if (!transient) { + // Folder or Multiroot workspace + if (URI.isUri(workspace)) { + this.profilesObject.workspaces.delete(workspace); + if (newProfile) { + this.profilesObject.workspaces.set(workspace, newProfile); + } + } + // Empty Window + else { + this.profilesObject.emptyWindows.delete(workspace); + if (newProfile) { + this.profilesObject.emptyWindows.set(workspace, newProfile); + } + } this.updateStoredProfileAssociations(); } } diff --git a/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts b/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts index a04c44f96ef..a9a7b3771c7 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, MutableDisposable, isDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, MutableDisposable, isDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IStorage, IStorageDatabase, Storage } from 'vs/base/parts/storage/common/storage'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { AbstractStorageService, IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget, isProfileUsingDefaultStorage } from 'vs/platform/storage/common/storage'; @@ -63,10 +63,16 @@ export abstract class AbstractUserDataProfileStorageService extends Disposable i readonly abstract onDidChange: Event; + private readonly storageServicesMap: DisposableMap | undefined; + constructor( + persistStorages: boolean, @IStorageService protected readonly storageService: IStorageService ) { super(); + if (persistStorages) { + this.storageServicesMap = this._register(new DisposableMap()); + } } async readStorageData(profile: IUserDataProfile): Promise> { @@ -82,16 +88,30 @@ export abstract class AbstractUserDataProfileStorageService extends Disposable i return fn(this.storageService); } - const storageDatabase = await this.createStorageDatabase(profile); - const storageService = new StorageService(storageDatabase); + let storageService = this.storageServicesMap?.get(profile.id); + if (!storageService) { + storageService = new StorageService(this.createStorageDatabase(profile)); + this.storageServicesMap?.set(profile.id, storageService); + + try { + await storageService.initialize(); + } catch (error) { + if (this.storageServicesMap?.has(profile.id)) { + this.storageServicesMap.deleteAndDispose(profile.id); + } else { + storageService.dispose(); + } + throw error; + } + } try { - await storageService.initialize(); const result = await fn(storageService); await storageService.flush(); return result; } finally { - storageService.dispose(); - await this.closeAndDispose(storageDatabase); + if (!this.storageServicesMap?.has(profile.id)) { + storageService.dispose(); + } } } @@ -111,16 +131,6 @@ export abstract class AbstractUserDataProfileStorageService extends Disposable i storageService.storeAll(Array.from(items.entries()).map(([key, value]) => ({ key, value, scope: StorageScope.PROFILE, target })), true); } - protected async closeAndDispose(storageDatabase: IStorageDatabase): Promise { - try { - await storageDatabase.close(); - } finally { - if (isDisposable(storageDatabase)) { - storageDatabase.dispose(); - } - } - } - protected abstract createStorageDatabase(profile: IUserDataProfile): Promise; } @@ -130,12 +140,13 @@ export class RemoteUserDataProfileStorageService extends AbstractUserDataProfile readonly onDidChange: Event; constructor( + persistStorages: boolean, private readonly remoteService: IRemoteService, userDataProfilesService: IUserDataProfilesService, storageService: IStorageService, logService: ILogService, ) { - super(storageService); + super(persistStorages, storageService); const channel = remoteService.getChannel('profileStorageListener'); const disposable = this._register(new MutableDisposable()); @@ -164,14 +175,26 @@ export class RemoteUserDataProfileStorageService extends AbstractUserDataProfile class StorageService extends AbstractStorageService { - private readonly profileStorage: IStorage; + private profileStorage: IStorage | undefined; - constructor(profileStorageDatabase: IStorageDatabase) { + constructor(private readonly profileStorageDatabase: Promise) { super({ flushInterval: 100 }); - this.profileStorage = this._register(new Storage(profileStorageDatabase)); } - protected doInitialize(): Promise { + protected async doInitialize(): Promise { + const profileStorageDatabase = await this.profileStorageDatabase; + const profileStorage = new Storage(profileStorageDatabase); + this._register(profileStorage.onDidChangeStorage(e => { + this.emitDidChangeValue(StorageScope.PROFILE, e); + })); + this._register(toDisposable(() => { + profileStorage.close(); + profileStorage.dispose(); + if (isDisposable(profileStorageDatabase)) { + profileStorageDatabase.dispose(); + } + })); + this.profileStorage = profileStorage; return this.profileStorage.init(); } diff --git a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts index 0679efcc742..313bedc9bff 100644 --- a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts +++ b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts @@ -18,7 +18,7 @@ export class NativeUserDataProfileStorageService extends RemoteUserDataProfileSt @IStorageService storageService: IStorageService, @ILogService logService: ILogService, ) { - super(mainProcessService, userDataProfilesService, storageService, logService); + super(false, mainProcessService, userDataProfilesService, storageService, logService); } } diff --git a/src/vs/platform/userDataProfile/node/userDataProfileStorageService.ts b/src/vs/platform/userDataProfile/node/userDataProfileStorageService.ts index 3b37d056aae..703011c9d60 100644 --- a/src/vs/platform/userDataProfile/node/userDataProfileStorageService.ts +++ b/src/vs/platform/userDataProfile/node/userDataProfileStorageService.ts @@ -9,7 +9,7 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService'; import { RemoteUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; -export class NativeUserDataProfileStorageService extends RemoteUserDataProfileStorageService { +export class SharedProcessUserDataProfileStorageService extends RemoteUserDataProfileStorageService { constructor( @IMainProcessService mainProcessService: IMainProcessService, @@ -17,6 +17,6 @@ export class NativeUserDataProfileStorageService extends RemoteUserDataProfileSt @IStorageService storageService: IStorageService, @ILogService logService: ILogService, ) { - super(mainProcessService, userDataProfilesService, storageService, logService); + super(true, mainProcessService, userDataProfilesService, storageService, logService); } } diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts index 7e898dd15fa..a7e2da87458 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; import { Schemas } from 'vs/base/common/network'; diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts index 48a68ee98a1..36cfb4efce5 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { InMemoryStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage'; @@ -43,7 +43,6 @@ export class TestUserDataProfileStorageService extends AbstractUserDataProfileSt return this.createStorageDatabase(profile); } - protected override async closeAndDispose(): Promise { } } suite('ProfileStorageService', () => { @@ -54,7 +53,7 @@ suite('ProfileStorageService', () => { let storage: Storage; setup(async () => { - testObject = disposables.add(new TestUserDataProfileStorageService(disposables.add(new InMemoryStorageService()))); + testObject = disposables.add(new TestUserDataProfileStorageService(false, disposables.add(new InMemoryStorageService()))); storage = disposables.add(new Storage(await testObject.setupStorageDatabase(profile))); await storage.init(); }); diff --git a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts index 515b2ce80da..8797fafc6c4 100644 --- a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts +++ b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; import { Schemas } from 'vs/base/common/network'; diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 53561e249db..7af2df9134a 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -533,7 +533,7 @@ export class LocalExtensionsProvider { addToSkipped.push(e); this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension`, gallery.displayName || gallery.identifier.id); } - if (error instanceof ExtensionManagementError && [ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleTargetPlatform].includes(error.code)) { + if (error instanceof ExtensionManagementError && [ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleApi, ExtensionManagementErrorCode.IncompatibleTargetPlatform].includes(error.code)) { this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension because the compatible extension is not found.`, gallery.displayName || gallery.identifier.id); } else if (error) { this.logService.error(error); diff --git a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts index 3436a31fb07..1ecc7b900a4 100644 --- a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; import { ILocalSyncExtension, ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync'; diff --git a/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts b/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts index 3c61cd98aca..7a17fac286d 100644 --- a/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; diff --git a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts index d27a2d8d6ac..7d3ecaca732 100644 --- a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; diff --git a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts index 935c553a872..c7a1f8675e7 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; import { TestUserDataSyncUtilService } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; diff --git a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts index 4799201077a..660185fd8fd 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IFileService } from 'vs/platform/files/common/files'; diff --git a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts index 274ac5ee696..625df21215c 100644 --- a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { addSetting, merge, updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import type { IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync'; diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts index 0cd110208b8..39246f25ce1 100644 --- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; diff --git a/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts b/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts index f41039cbb9e..50e5caa545f 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { merge } from 'vs/platform/userDataSync/common/snippetsMerge'; diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts index fc9644c2245..97f4ab83f13 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { IStringDictionary } from 'vs/base/common/collections'; import { dirname, joinPath } from 'vs/base/common/resources'; diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 1e854038a9d..a336ca32411 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Barrier } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; diff --git a/src/vs/platform/userDataSync/test/common/tasksSync.test.ts b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts index 73fe0a6b23d..c6cfd18a44b 100644 --- a/src/vs/platform/userDataSync/test/common/tasksSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index 47cde8003c7..e9c86afd301 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { joinPath } from 'vs/base/common/resources'; diff --git a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts index 40cc6f3623d..e60f5314799 100644 --- a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IUserDataProfile, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; diff --git a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts index 2d6d7de2bf0..2227e3d1182 100644 --- a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index b622a3efe44..3a035f63221 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -95,7 +95,7 @@ export class UserDataSyncClient extends Disposable { const storageService = this._register(new TestStorageService(userDataProfilesService.defaultProfile)); this.instantiationService.stub(IStorageService, this._register(storageService)); - this.instantiationService.stub(IUserDataProfileStorageService, this._register(new TestUserDataProfileStorageService(storageService))); + this.instantiationService.stub(IUserDataProfileStorageService, this._register(new TestUserDataProfileStorageService(false, storageService))); const configurationService = this._register(new ConfigurationService(userDataProfilesService.defaultProfile.settingsResource, fileService, new NullPolicyService(), logService)); await configurationService.initialize(); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 665d09ed41e..a06d711277e 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { dirname, joinPath } from 'vs/base/common/resources'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts index db83d62e163..2a69d9d3ebf 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { newWriteableBufferStream } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 16eef424015..2b7ffc4651f 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -49,6 +49,9 @@ export interface IBaseOpenWindowsOptions { * If not set, defaults to the remote authority of the current window. */ readonly remoteAuthority?: string | null; + + readonly forceProfile?: string; + readonly forceTempProfile?: boolean; } export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { @@ -64,9 +67,6 @@ export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { readonly gotoLineMode?: boolean; readonly waitMarkerFileURI?: URI; - - readonly forceProfile?: string; - readonly forceTempProfile?: boolean; } export interface IAddFoldersRequest { diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 45131cdc6d6..64cafc50ed7 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -971,7 +971,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { } // Proxy - if (!e || e.affectsConfiguration('http.proxy')) { + if (!e || e.affectsConfiguration('http.proxy') || e.affectsConfiguration('http.noProxy')) { let newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() || (process.env['https_proxy'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['HTTP_PROXY'] || '').trim() // Not standardized. || undefined; @@ -980,7 +980,8 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { newHttpProxy = newHttpProxy.substr(0, newHttpProxy.length - 1); } - const newNoProxy = (process.env['no_proxy'] || process.env['NO_PROXY'] || '').trim() || undefined; // Not standardized. + const newNoProxy = (this.configurationService.getValue('http.noProxy') || []).map((item) => item.trim()).join(',') + || (process.env['no_proxy'] || process.env['NO_PROXY'] || '').trim() || undefined; // Not standardized. if ((newHttpProxy || '').indexOf('@') === -1 && (newHttpProxy !== this.currentHttpProxy || newNoProxy !== this.currentNoProxy)) { this.currentHttpProxy = newHttpProxy; this.currentNoProxy = newNoProxy; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 8c9cb2b1868..fd704a5cfe6 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -269,7 +269,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const forceReuseWindow = options?.forceReuseWindow; const forceNewWindow = !forceReuseWindow; - return this.open({ ...openConfig, cli, forceEmpty, forceNewWindow, forceReuseWindow, remoteAuthority }); + return this.open({ ...openConfig, cli, forceEmpty, forceNewWindow, forceReuseWindow, remoteAuthority, forceTempProfile: options?.forceTempProfile, forceProfile: options?.forceProfile }); } openExistingWindow(window: ICodeWindow, openConfig: IOpenConfiguration): void { diff --git a/src/vs/platform/windows/electron-main/windowsStateHandler.ts b/src/vs/platform/windows/electron-main/windowsStateHandler.ts index da40e6d202d..88fdd40326d 100644 --- a/src/vs/platform/windows/electron-main/windowsStateHandler.ts +++ b/src/vs/platform/windows/electron-main/windowsStateHandler.ts @@ -85,19 +85,19 @@ export class WindowsStateHandler extends Disposable { }); // Handle various lifecycle events around windows - this.lifecycleMainService.onBeforeCloseWindow(window => this.onBeforeCloseWindow(window)); - this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown()); - this.windowsMainService.onDidChangeWindowsCount(e => { + this._register(this.lifecycleMainService.onBeforeCloseWindow(window => this.onBeforeCloseWindow(window))); + this._register(this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown())); + this._register(this.windowsMainService.onDidChangeWindowsCount(e => { if (e.newCount - e.oldCount > 0) { // clear last closed window state when a new window opens. this helps on macOS where // otherwise closing the last window, opening a new window and then quitting would // use the state of the previously closed window when restarting. this.lastClosedState = undefined; } - }); + })); // try to save state before destroy because close will not fire - this.windowsMainService.onDidDestroyWindow(window => this.onBeforeCloseWindow(window)); + this._register(this.windowsMainService.onDidDestroyWindow(window => this.onBeforeCloseWindow(window))); } // Note that onBeforeShutdown() and onBeforeCloseWindow() are fired in different order depending on the OS: diff --git a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts index 490d6858a62..f3ce84f9c7a 100644 --- a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { join } from 'vs/base/common/path'; diff --git a/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts index 0b96b1bf740..65d1a9937f9 100644 --- a/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { tmpdir } from 'os'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/workspace/test/common/workspace.test.ts b/src/vs/platform/workspace/test/common/workspace.test.ts index 2464d5e4a35..fb8c1cf18e6 100644 --- a/src/vs/platform/workspace/test/common/workspace.test.ts +++ b/src/vs/platform/workspace/test/common/workspace.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { join } from 'vs/base/common/path'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; diff --git a/src/vs/platform/workspaces/test/common/workspaces.test.ts b/src/vs/platform/workspaces/test/common/workspaces.test.ts index a1f2f262d00..a08f1035ce1 100644 --- a/src/vs/platform/workspaces/test/common/workspaces.test.ts +++ b/src/vs/platform/workspaces/test/common/workspaces.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ISerializedSingleFolderWorkspaceIdentifier, ISerializedWorkspaceIdentifier, reviveIdentifier, hasWorkspaceFileExtension, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IEmptyWorkspaceIdentifier, toWorkspaceIdentifier, isEmptyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; diff --git a/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts b/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts index 56e4d92fdc9..42fd463b9bf 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'vs/base/common/path'; diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts index 2677ffba87d..f2102c3b098 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { tmpdir } from 'os'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts index 0234ce57bb9..fe5bd2994c8 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import * as os from 'os'; import { isUNC, toSlashes } from 'vs/base/common/extpath'; diff --git a/src/vs/server/node/extensionHostConnection.ts b/src/vs/server/node/extensionHostConnection.ts index f259ea2cbaf..9fbb33d3ece 100644 --- a/src/vs/server/node/extensionHostConnection.ts +++ b/src/vs/server/node/extensionHostConnection.ts @@ -5,23 +5,23 @@ import * as cp from 'child_process'; import * as net from 'net'; -import { getNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; -import { FileAccess } from 'vs/base/common/network'; -import { join, delimiter } from 'vs/base/common/path'; import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { FileAccess } from 'vs/base/common/network'; +import { delimiter, join } from 'vs/base/common/path'; +import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; +import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { createRandomIPCHandle, NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remoteAgentConnection'; -import { IExtHostReadyMessage, IExtHostSocketMessage, IExtHostReduceGraceTimeMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; -import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; -import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; -import { removeDangerousEnvVariables } from 'vs/base/common/processes'; +import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; -import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { IPCExtHostConnection, writeExtHostConnection, SocketExtHostConnection } from 'vs/workbench/services/extensions/common/extensionHostEnv'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { getNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; +import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; +import { IPCExtHostConnection, SocketExtHostConnection, writeExtHostConnection } from 'vs/workbench/services/extensions/common/extensionHostEnv'; +import { IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, IExtHostSocketMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; export async function buildUserEnvironment(startParamsEnv: { [key: string]: string | null } = {}, withUserShellEnvironment: boolean, language: string, environmentService: IServerEnvironmentService, logService: ILogService, configurationService: IConfigurationService): Promise { const nlsConfig = await getNLSConfiguration(language, environmentService.userDataPath); @@ -103,7 +103,7 @@ class ConnectionData { } } -export class ExtensionHostConnection { +export class ExtensionHostConnection extends Disposable { private _onClose = new Emitter(); readonly onClose: Event = this._onClose.event; @@ -124,6 +124,7 @@ export class ExtensionHostConnection { @IExtensionHostStatusService private readonly _extensionHostStatusService: IExtensionHostStatusService, @IConfigurationService private readonly _configurationService: IConfigurationService ) { + super(); this._canSendSocket = (!isWindows || !this._environmentService.args['socket-path']); this._disposed = false; this._remoteAddress = remoteAddress; @@ -133,6 +134,11 @@ export class ExtensionHostConnection { this._log(`New connection established.`); } + override dispose(): void { + this._cleanResources(); + super.dispose(); + } + private get _logPrefix(): string { return `[${this._remoteAddress}][${this._reconnectionToken.substr(0, 8)}][ExtensionHostConnection] `; } @@ -271,8 +277,8 @@ export class ExtensionHostConnection { this._extensionHostProcess.stderr!.setEncoding('utf8'); const onStdout = Event.fromNodeEventEmitter(this._extensionHostProcess.stdout!, 'data'); const onStderr = Event.fromNodeEventEmitter(this._extensionHostProcess.stderr!, 'data'); - onStdout((e) => this._log(`<${pid}> ${e}`)); - onStderr((e) => this._log(`<${pid}> ${e}`)); + this._register(onStdout((e) => this._log(`<${pid}> ${e}`))); + this._register(onStderr((e) => this._log(`<${pid}> ${e}`))); // Lifecycle this._extensionHostProcess.on('error', (err) => { diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 38fd510a3bc..cc5e9ee474a 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -512,6 +512,7 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { this._extHostConnections[reconnectionToken] = con; this._allReconnectionTokens.add(reconnectionToken); con.onClose(() => { + con.dispose(); delete this._extHostConnections[reconnectionToken]; this._onDidCloseExtHostConnection(); }); diff --git a/src/vs/server/node/remoteExtensionsScanner.ts b/src/vs/server/node/remoteExtensionsScanner.ts index 19a50f7b067..7b58140ea1b 100644 --- a/src/vs/server/node/remoteExtensionsScanner.ts +++ b/src/vs/server/node/remoteExtensionsScanner.ts @@ -103,26 +103,6 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return extensions; } - async scanSingleExtension(extensionLocation: URI, isBuiltin: boolean, language?: string): Promise { - await this._whenBuiltinExtensionsReady; - - const extensionPath = extensionLocation.scheme === Schemas.file ? extensionLocation.fsPath : null; - - if (!extensionPath) { - return null; - } - - const extension = await this._scanSingleExtension(extensionPath, isBuiltin, language ?? platform.language); - - if (!extension) { - return null; - } - - this._massageWhenConditions([extension]); - - return extension; - } - private async _scanExtensions(profileLocation: URI, language: string, workspaceInstalledExtensionLocations: URI[] | undefined, extensionDevelopmentPath: string[] | undefined, languagePackId: string | undefined): Promise { await this._ensureLanguagePackIsInstalled(language, languagePackId); @@ -168,13 +148,6 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return scannedExtensions.map(e => toExtensionDescription(e, false)); } - private async _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string): Promise { - const extensionLocation = URI.file(resolve(extensionPath)); - const type = isBuiltin ? ExtensionType.System : ExtensionType.User; - const scannedExtension = await this._extensionsScannerService.scanExistingExtension(extensionLocation, type, { language }); - return scannedExtension ? toExtensionDescription(scannedExtension, false) : null; - } - private async _ensureLanguagePackIsInstalled(language: string, languagePackId: string | undefined): Promise { if ( // No need to install language packs for the default language @@ -351,10 +324,6 @@ export class RemoteExtensionsScannerChannel implements IServerChannel { ); return extensions.map(extension => transformOutgoingURIs(extension, uriTransformer)); } - case 'scanSingleExtension': { - const extension = await this.service.scanSingleExtension(URI.revive(uriTransformer.transformIncoming(args[0])), args[1], args[2]); - return extension ? transformOutgoingURIs(extension, uriTransformer) : null; - } } throw new Error('Invalid call'); } diff --git a/src/vs/server/test/node/serverConnectionToken.test.ts b/src/vs/server/test/node/serverConnectionToken.test.ts index b624dd6d676..b04dab4d308 100644 --- a/src/vs/server/test/node/serverConnectionToken.test.ts +++ b/src/vs/server/test/node/serverConnectionToken.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index bf97a663f61..a49081a78f1 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -20,6 +20,7 @@ import './mainThreadBulkEdits'; import './mainThreadLanguageModels'; import './mainThreadChatAgents2'; import './mainThreadChatVariables'; +import './mainThreadLanguageModelTools'; import './mainThreadEmbeddings'; import './mainThreadCodeInsets'; import './mainThreadCLICommands'; diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 3719825841a..5780ec8797e 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -6,7 +6,7 @@ import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, INTERNAL_AUTH_PROVIDER_PREFIX as INTERNAL_MODEL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, INTERNAL_AUTH_PROVIDER_PREFIX as INTERNAL_MODEL_AUTH_PROVIDER_PREFIX, AuthenticationSessionAccount, IAuthenticationProviderSessionOptions } from 'vs/workbench/services/authentication/common/authentication'; import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; @@ -31,6 +31,7 @@ interface AuthenticationGetSessionOptions { createIfNone?: boolean; forceNewSession?: boolean | AuthenticationForceNewSessionOptions; silent?: boolean; + account?: AuthenticationSessionAccount; } export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider { @@ -49,8 +50,8 @@ export class MainThreadAuthenticationProvider extends Disposable implements IAut this.onDidChangeSessions = onDidChangeSessionsEmitter.event; } - async getSessions(scopes?: string[]) { - return this._proxy.$getSessions(this.id, scopes); + async getSessions(scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions) { + return this._proxy.$getSessions(this.id, scopes, options); } createSession(scopes: string[], options: IAuthenticationCreateSessionOptions): Promise { @@ -159,7 +160,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { - const sessions = await this.authenticationService.getSessions(providerId, scopes, true); + const sessions = await this.authenticationService.getSessions(providerId, scopes, options.account, true); const provider = this.authenticationService.getProvider(providerId); // Error cases @@ -213,18 +214,16 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu let session; if (sessions?.length && !options.forceNewSession) { - session = provider.supportsMultipleAccounts + session = provider.supportsMultipleAccounts && !options.account ? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopes, sessions) : sessions[0]; } else { - let sessionToRecreate: AuthenticationSession | undefined; - if (typeof options.forceNewSession === 'object' && options.forceNewSession.sessionToRecreate) { - sessionToRecreate = options.forceNewSession.sessionToRecreate as AuthenticationSession; - } else { + let account: AuthenticationSessionAccount | undefined = options.account; + if (!account) { const sessionIdToRecreate = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); - sessionToRecreate = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate) : undefined; + account = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate)?.account : undefined; } - session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, sessionToRecreate }); + session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, account }); } this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]); @@ -261,16 +260,17 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu return session; } - async $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise { - const sessions = await this.authenticationService.getSessions(providerId, [...scopes], true); - const accessibleSessions = sessions.filter(s => this.authenticationAccessService.isAccessAllowed(providerId, s.account.label, extensionId)); - if (accessibleSessions.length) { - this.sendProviderUsageTelemetry(extensionId, providerId); - for (const session of accessibleSessions) { - this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); + async $getAccounts(providerId: string): Promise> { + const sessions = await this.authenticationService.getSessions(providerId); + const accounts = new Array(); + const seenAccounts = new Set(); + for (const session of sessions) { + if (!seenAccounts.has(session.account.label)) { + seenAccounts.add(session.account.label); + accounts.push(session.account); } } - return accessibleSessions; + return accounts; } private sendProviderUsageTelemetry(extensionId: string, providerId: string): void { diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 0d63847dae5..35126b7998d 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -199,7 +199,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void { const data = this._agents.get(handle); if (!data) { - throw new Error(`No agent with handle ${handle} registered`); + this._logService.error(`MainThreadChatAgents2#$updateAgent: No agent with handle ${handle} registered`); + return; } data.hasFollowups = metadataUpdate.hasFollowups; this._chatAgentService.updateAgent(data.id, revive(metadataUpdate)); diff --git a/src/vs/workbench/api/browser/mainThreadChatVariables.ts b/src/vs/workbench/api/browser/mainThreadChatVariables.ts index bf7103206a0..9e08e5d1423 100644 --- a/src/vs/workbench/api/browser/mainThreadChatVariables.ts +++ b/src/vs/workbench/api/browser/mainThreadChatVariables.ts @@ -5,10 +5,7 @@ import { DisposableMap } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; -import { URI } from 'vs/base/common/uri'; -import { Location } from 'vs/editor/common/languages'; import { ExtHostChatVariablesShape, ExtHostContext, IChatVariableResolverProgressDto, MainContext, MainThreadChatVariablesShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress, IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -50,8 +47,4 @@ export class MainThreadChatVariables implements MainThreadChatVariablesShape { $unregisterVariable(handle: number): void { this._variables.deleteAndDispose(handle); } - - $attachContext(name: string, value: string | URI | Location | unknown, location: ChatAgentLocation.Panel): void { - this._chatVariablesService.attachContext(name, revive(value), location); - } } diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 4eb6385cbed..6bbeb159248 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -27,6 +27,10 @@ import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { Schemas } from 'vs/base/common/network'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { MarshalledCommentThread } from 'vs/workbench/common/comments'; +import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { CommentThreadRevealOptions } from 'vs/editor/common/languages'; export class MainThreadCommentThread implements languages.CommentThread { private _input?: languages.CommentInput; @@ -181,6 +185,7 @@ export class MainThreadCommentThread implements languages.CommentThread { public threadId: string, public resource: string, private _range: T | undefined, + comments: languages.Comment[] | undefined, private _canReply: boolean, private _isTemplate: boolean, public editorId?: string @@ -188,6 +193,8 @@ export class MainThreadCommentThread implements languages.CommentThread { this._isDisposed = false; if (_isTemplate) { this.comments = []; + } else if (comments) { + this._comments = comments; } } @@ -294,6 +301,7 @@ export class MainThreadCommentController implements ICommentController { threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, + comments: languages.Comment[], isTemplate: boolean, editorId?: string ): languages.CommentThread { @@ -304,6 +312,7 @@ export class MainThreadCommentController implements ICommentController { threadId, URI.revive(resource).toString(), range, + comments, true, isTemplate, editorId @@ -520,7 +529,9 @@ export class MainThreadComments extends Disposable implements MainThreadComments extHostContext: IExtHostContext, @ICommentService private readonly _commentService: ICommentService, @IViewsService private readonly _viewsService: IViewsService, - @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService + @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, + @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, + @IEditorService private readonly _editorService: IEditorService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments); @@ -584,6 +595,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, + comments: languages.Comment[], extensionId: ExtensionIdentifier, isTemplate: boolean, editorId?: string @@ -594,7 +606,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments return undefined; } - return provider.createCommentThread(extensionId.value, commentThreadHandle, threadId, resource, range, isTemplate, editorId); + return provider.createCommentThread(extensionId.value, commentThreadHandle, threadId, resource, range, comments, isTemplate, editorId); } $updateCommentThread(handle: number, @@ -631,6 +643,21 @@ export class MainThreadComments extends Disposable implements MainThreadComments provider.updateCommentingRanges(resourceHints); } + async $revealCommentThread(handle: number, commentThreadHandle: number, options: CommentThreadRevealOptions): Promise { + const provider = this._commentControllers.get(handle); + + if (!provider) { + return Promise.resolve(); + } + + const thread = provider.getAllComments().find(thread => thread.commentThreadHandle === commentThreadHandle); + if (!thread || !thread.isDocumentCommentThread()) { + return Promise.resolve(); + } + + revealCommentThread(this._commentService, this._editorService, this._uriIdentityService, thread, undefined, options.focusReply, undefined, options.preserveFocus); + } + private registerView(commentsViewAlreadyRegistered: boolean) { if (!commentsViewAlreadyRegistered) { const VIEW_CONTAINER: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index f58ba4c47fb..36b5a4df0ae 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -329,6 +329,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb compact: options.compact, compoundRoot: parentSession?.compoundRoot, saveBeforeRestart: saveBeforeStart, + testRun: options.testRun, suppressDebugStatusbar: options.suppressDebugStatusbar, suppressDebugToolbar: options.suppressDebugToolbar, diff --git a/src/vs/workbench/api/browser/mainThreadErrors.ts b/src/vs/workbench/api/browser/mainThreadErrors.ts index 2d05a6a0e44..e1591179944 100644 --- a/src/vs/workbench/api/browser/mainThreadErrors.ts +++ b/src/vs/workbench/api/browser/mainThreadErrors.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SerializedError, onUnexpectedError, ErrorNoTelemetry } from 'vs/base/common/errors'; +import { SerializedError, onUnexpectedError, transformErrorFromSerialization } from 'vs/base/common/errors'; import { extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { MainContext, MainThreadErrorsShape } from 'vs/workbench/api/common/extHost.protocol'; @@ -16,11 +16,7 @@ export class MainThreadErrors implements MainThreadErrorsShape { $onUnexpectedError(err: any | SerializedError): void { if (err && err.$isError) { - const { name, message, stack } = err; - err = err.noTelemetry ? new ErrorNoTelemetry() : new Error(); - err.message = message; - err.name = name; - err.stack = stack; + err = transformErrorFromSerialization(err); } onUnexpectedError(err); } diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 6732d38f5e6..7c4db0a5def 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -6,7 +6,7 @@ import { Action } from 'vs/base/common/actions'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { SerializedError } from 'vs/base/common/errors'; +import { SerializedError, transformErrorFromSerialization } from 'vs/base/common/errors'; import { FileAccess } from 'vs/base/common/network'; import Severity from 'vs/base/common/severity'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -73,19 +73,13 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha this._internalExtensionService._onDidActivateExtension(extensionId, codeLoadingTime, activateCallTime, activateResolvedTime, activationReason); } $onExtensionRuntimeError(extensionId: ExtensionIdentifier, data: SerializedError): void { - const error = new Error(); - error.name = data.name; - error.message = data.message; - error.stack = data.stack; + const error = transformErrorFromSerialization(data); this._internalExtensionService._onExtensionRuntimeError(extensionId, error); console.error(`[${extensionId.value}]${error.message}`); console.error(error.stack); } async $onExtensionActivationError(extensionId: ExtensionIdentifier, data: SerializedError, missingExtensionDependency: MissingExtensionDependency | null): Promise { - const error = new Error(); - error.name = data.name; - error.message = data.message; - error.stack = data.stack; + const error = transformErrorFromSerialization(data); this._internalExtensionService._onDidActivateExtensionError(extensionId, error); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 1bc3a3096b9..fe465d7fc09 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -612,6 +612,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread provideInlineCompletions: async (model: ITextModel, position: EditorPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise => { return this._proxy.$provideInlineCompletions(handle, model.uri, position, context, token); }, + provideInlineEdits: async (model: ITextModel, range: EditorRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise => { + return this._proxy.$provideInlineEdits(handle, model.uri, range, context, token); + }, handleItemDidShow: async (completions: IdentifiableInlineCompletions, item: IdentifiableInlineCompletion, updatedInsertText: string): Promise => { if (supportsHandleEvents) { await this._proxy.$handleInlineCompletionDidShow(handle, completions.pid, item.idx, updatedInsertText); @@ -1142,7 +1145,7 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentDropEdit } } - async provideDocumentDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + async provideDocumentDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { const request = this.dataTransfers.add(dataTransfer); try { const dataTransferDto = await typeConvert.DataTransfer.from(dataTransfer); @@ -1155,14 +1158,19 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentDropEdit return; } - return edits.map(edit => { - return { - ...edit, - yieldTo: edit.yieldTo?.map(x => ({ kind: new HierarchicalKind(x) })), - kind: edit.kind ? new HierarchicalKind(edit.kind) : undefined, - additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)), - }; - }); + return { + edits: edits.map(edit => { + return { + ...edit, + yieldTo: edit.yieldTo?.map(x => ({ kind: new HierarchicalKind(x) })), + kind: edit.kind ? new HierarchicalKind(edit.kind) : undefined, + additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)), + }; + }), + dispose: () => { + this._proxy.$releaseDocumentOnDropEdits(this._handle, request.id); + }, + }; } finally { request.dispose(); } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts new file mode 100644 index 00000000000..fbda6ced5a3 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; +import { ExtHostLanguageModelToolsShape, ExtHostContext, MainContext, MainThreadLanguageModelToolsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IToolData, ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; + +@extHostNamedCustomer(MainContext.MainThreadLanguageModelTools) +export class MainThreadLanguageModelTools extends Disposable implements MainThreadLanguageModelToolsShape { + + private readonly _proxy: ExtHostLanguageModelToolsShape; + private readonly _tools = this._register(new DisposableMap()); + + constructor( + extHostContext: IExtHostContext, + @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, + ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostLanguageModelTools); + + this._register(this._languageModelToolsService.onDidChangeTools(e => this._proxy.$acceptToolDelta(e))); + } + + async $getTools(): Promise { + return Array.from(this._languageModelToolsService.getTools()); + } + + $invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + return this._languageModelToolsService.invokeTool(name, parameters, token); + } + + $registerTool(name: string): void { + const disposable = this._languageModelToolsService.registerToolImplementation( + name, + { + invoke: async (parameters, token) => { + return await this._proxy.$invokeTool(name, parameters, token); + }, + }); + this._tools.set(name, disposable); + } + + $unregisterTool(name: string): void { + this._tools.deleteAndDispose(name); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index 9b14928a514..adea665487c 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -3,18 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { AsyncIterableSource, DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { SerializedError, transformErrorForSerialization, transformErrorFromSerialization } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProgress, Progress } from 'vs/platform/progress/common/progress'; import { ExtHostLanguageModelsShape, ExtHostContext, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ILanguageModelStatsService } from 'vs/workbench/contrib/chat/common/languageModelStats'; -import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage, ILanguageModelChatSelector } from 'vs/workbench/contrib/chat/common/languageModels'; +import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage, ILanguageModelChatSelector, ILanguageModelChatResponse } from 'vs/workbench/contrib/chat/common/languageModels'; import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; -import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationProviderCreateSessionOptions, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; +import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -24,7 +25,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { private readonly _proxy: ExtHostLanguageModelsShape; private readonly _store = new DisposableStore(); private readonly _providerRegistrations = new DisposableMap(); - private readonly _pendingProgress = new Map>(); + private readonly _pendingProgress = new Map; stream: AsyncIterableSource }>(); constructor( extHostContext: IExtHostContext, @@ -49,14 +50,23 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { const dipsosables = new DisposableStore(); dipsosables.add(this._chatProviderService.registerLanguageModelChat(identifier, { metadata, - provideChatResponse: async (messages, from, options, progress, token) => { + sendChatRequest: async (messages, from, options, token) => { const requestId = (Math.random() * 1e6) | 0; - this._pendingProgress.set(requestId, progress); + const defer = new DeferredPromise(); + const stream = new AsyncIterableSource(); + try { - await this._proxy.$provideLanguageModelResponse(handle, requestId, from, messages, options, token); - } finally { + this._pendingProgress.set(requestId, { defer, stream }); + await this._proxy.$startChatRequest(handle, requestId, from, messages, options, token); + } catch (err) { this._pendingProgress.delete(requestId); + throw err; } + + return { + result: defer.p, + stream: stream.asyncIterable + } satisfies ILanguageModelChatResponse; }, provideTokenCount: (str, token) => { return this._proxy.$provideTokenLength(handle, str, token); @@ -68,8 +78,28 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { this._providerRegistrations.set(handle, dipsosables); } - async $handleProgressChunk(requestId: number, chunk: IChatResponseFragment): Promise { - this._pendingProgress.get(requestId)?.report(chunk); + async $reportResponsePart(requestId: number, chunk: IChatResponseFragment): Promise { + const data = this._pendingProgress.get(requestId); + this._logService.trace('[LM] report response PART', Boolean(data), requestId, chunk); + if (data) { + data.stream.emitOne(chunk); + } + } + + async $reportResponseDone(requestId: number, err: SerializedError | undefined): Promise { + const data = this._pendingProgress.get(requestId); + this._logService.trace('[LM] report response DONE', Boolean(data), requestId, err); + if (data) { + this._pendingProgress.delete(requestId); + if (err) { + const error = transformErrorFromSerialization(err); + data.stream.reject(error); + data.defer.error(error); + } else { + data.stream.resolve(); + data.defer.complete(undefined); + } + } } $unregisterProvider(handle: number): void { @@ -84,21 +114,36 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { this._languageModelStatsService.update(identifier, extensionId, participant, tokenCount); } - async $fetchResponse(extension: ExtensionIdentifier, providerId: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise { - this._logService.debug('[CHAT] extension request STARTED', extension.value, requestId); + async $tryStartChatRequest(extension: ExtensionIdentifier, providerId: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise { + this._logService.trace('[CHAT] request STARTED', extension.value, requestId); - const task = this._chatProviderService.makeLanguageModelChatRequest(providerId, extension, messages, options, new Progress(value => { - this._proxy.$handleResponseFragment(requestId, value); - }), token); + const response = await this._chatProviderService.sendChatRequest(providerId, extension, messages, options, token); - task.catch(err => { - this._logService.error('[CHAT] extension request ERRORED', err, extension.value, requestId); - throw err; - }).finally(() => { + // !!! IMPORTANT !!! + // This method must return before the response is done (has streamed all parts) + // and because of that we consume the stream without awaiting + // !!! IMPORTANT !!! + const streaming = (async () => { + try { + for await (const part of response.stream) { + this._logService.trace('[CHAT] request PART', extension.value, requestId, part); + await this._proxy.$acceptResponsePart(requestId, part); + } + this._logService.trace('[CHAT] request DONE', extension.value, requestId); + } catch (err) { + this._logService.error('[CHAT] extension request ERRORED in STREAM', err, extension.value, requestId); + this._proxy.$acceptResponseDone(requestId, transformErrorForSerialization(err)); + } + })(); + + // When the response is done (signaled via its result) we tell the EH + Promise.allSettled([response.result, streaming]).then(() => { this._logService.debug('[CHAT] extension request DONE', extension.value, requestId); + this._proxy.$acceptResponseDone(requestId, undefined); + }, err => { + this._logService.error('[CHAT] extension request ERRORED', err, extension.value, requestId); + this._proxy.$acceptResponseDone(requestId, transformErrorForSerialization(err)); }); - - return task; } @@ -161,9 +206,9 @@ class LanguageModelAccessAuthProvider implements IAuthenticationProvider { if (this._session) { return [this._session]; } - return [await this.createSession(scopes || [], {})]; + return [await this.createSession(scopes || [])]; } - async createSession(scopes: string[], options: IAuthenticationProviderCreateSessionOptions): Promise { + async createSession(scopes: string[]): Promise { this._session = this._createFakeSession(scopes); this._onDidChangeSessions.fire({ added: [this._session], changed: [], removed: [] }); return this._session; diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 348cd234e76..5ae48f45106 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Barrier } from 'vs/base/common/async'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; +import { observableValue } from 'vs/base/common/observable'; import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; -import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto } from '../common/extHost.protocol'; +import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto } from '../common/extHost.protocol'; import { Command } from 'vs/editor/common/languages'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -25,6 +27,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { Schemas } from 'vs/base/common/network'; import { ITextModel } from 'vs/editor/common/model'; +import { ILogService } from 'vs/platform/log/common/log'; function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (iconDto === undefined) { @@ -39,6 +42,13 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da } } +function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem { + const icon = getIconFromIconDto(historyItemDto.icon); + const labels = historyItemDto.labels?.map(l => ({ title: l.title, icon: getIconFromIconDto(l.icon) })); + + return { ...historyItemDto, icon, labels }; +} + class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider { constructor( textModelService: ITextModelService, @@ -162,6 +172,9 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { this._onDidChangeCurrentHistoryItemGroup.fire(); } + private readonly _currentHistoryItemGroupObs = observableValue(this, undefined); + get currentHistoryItemGroupObs() { return this._currentHistoryItemGroupObs; } + constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { } async resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined> { @@ -170,12 +183,17 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { async provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise { const historyItems = await this.proxy.$provideHistoryItems(this.handle, historyItemGroupId, options, CancellationToken.None); - return historyItems?.map(historyItem => ({ ...historyItem, icon: getIconFromIconDto(historyItem.icon) })); + return historyItems?.map(historyItem => toISCMHistoryItem(historyItem)); + } + + async provideHistoryItems2(options: ISCMHistoryOptions): Promise { + const historyItems = await this.proxy.$provideHistoryItems2(this.handle, options, CancellationToken.None); + return historyItems?.map(historyItem => toISCMHistoryItem(historyItem)); } async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise { const historyItem = await this.proxy.$provideHistoryItemSummary(this.handle, historyItemId, historyItemParentId, CancellationToken.None); - return historyItem ? { ...historyItem, icon: getIconFromIconDto(historyItem.icon) } : undefined; + return historyItem ? toISCMHistoryItem(historyItem) : undefined; } async provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise { @@ -188,6 +206,9 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { })); } + $onDidChangeCurrentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined): void { + this._currentHistoryItemGroupObs.set(historyItemGroup, undefined); + } } class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { @@ -224,21 +245,21 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { get inputBoxTextModel(): ITextModel { return this._inputBoxTextModel; } get contextValue(): string { return this._providerId; } - get commitTemplate(): string { return this.features.commitTemplate || ''; } get historyProvider(): ISCMHistoryProvider | undefined { return this._historyProvider; } get acceptInputCommand(): Command | undefined { return this.features.acceptInputCommand; } get actionButton(): ISCMActionButtonDescriptor | undefined { return this.features.actionButton ?? undefined; } - get statusBarCommands(): Command[] | undefined { return this.features.statusBarCommands; } - get count(): number | undefined { return this.features.count; } + + private readonly _count = observableValue(this, undefined); + get count() { return this._count; } + + private readonly _statusBarCommands = observableValue(this, undefined); + get statusBarCommands() { return this._statusBarCommands; } private readonly _name: string | undefined; get name(): string { return this._name ?? this._label; } - private readonly _onDidChangeCommitTemplate = new Emitter(); - readonly onDidChangeCommitTemplate: Event = this._onDidChangeCommitTemplate.event; - - private readonly _onDidChangeStatusBarCommands = new Emitter(); - get onDidChangeStatusBarCommands(): Event { return this._onDidChangeStatusBarCommands.event; } + private readonly _commitTemplate = observableValue(this, ''); + get commitTemplate() { return this._commitTemplate; } private readonly _onDidChangeHistoryProvider = new Emitter(); readonly onDidChangeHistoryProvider: Event = this._onDidChangeHistoryProvider.event; @@ -250,6 +271,8 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { public readonly isSCM: boolean = true; private _historyProvider: ISCMHistoryProvider | undefined; + private readonly _historyProviderObs = observableValue(this, undefined); + get historyProviderObs() { return this._historyProviderObs; } constructor( private readonly proxy: ExtHostSCMShape, @@ -260,7 +283,8 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private readonly _inputBoxTextModel: ITextModel, private readonly _quickDiffService: IQuickDiffService, private readonly _uriIdentService: IUriIdentityService, - private readonly _workspaceContextService: IWorkspaceContextService + private readonly _workspaceContextService: IWorkspaceContextService, + private readonly _logService: ILogService ) { if (_rootUri) { const folder = this._workspaceContextService.getWorkspaceFolder(_rootUri); @@ -277,11 +301,16 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { this._onDidChange.fire(); if (typeof features.commitTemplate !== 'undefined') { - this._onDidChangeCommitTemplate.fire(this.commitTemplate); + this._commitTemplate.set(features.commitTemplate, undefined); + } + + if (typeof features.count !== 'undefined') { + this._count.set(features.count, undefined); } if (typeof features.statusBarCommands !== 'undefined') { - this._onDidChangeStatusBarCommands.fire(this.statusBarCommands!); + this._logService.trace(`MainThreadSCMProvider#updateSourceControl (${this._id}): ${features.statusBarCommands.map(c => c.title).join(', ')}`); + this._statusBarCommands.set(features.statusBarCommands, undefined); } if (features.hasQuickDiffProvider && !this._quickDiff) { @@ -297,9 +326,14 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { } if (features.hasHistoryProvider && !this._historyProvider) { - this._historyProvider = new MainThreadSCMHistoryProvider(this.proxy, this.handle); + const historyProvider = new MainThreadSCMHistoryProvider(this.proxy, this.handle); + this._historyProviderObs.set(historyProvider, undefined); + + this._historyProvider = historyProvider; this._onDidChangeHistoryProvider.fire(); } else if (features.hasHistoryProvider === false && this._historyProvider) { + this._historyProviderObs.set(undefined, undefined); + this._historyProvider = undefined; this._onDidChangeHistoryProvider.fire(); } @@ -423,6 +457,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { } this._historyProvider.currentHistoryItemGroup = currentHistoryItemGroup ?? undefined; + this._historyProviderObs.get()?.$onDidChangeCurrentHistoryItemGroup(currentHistoryItemGroup); } toJSON(): any { @@ -442,6 +477,7 @@ export class MainThreadSCM implements MainThreadSCMShape { private readonly _proxy: ExtHostSCMShape; private _repositories = new Map(); + private _repositoryBarriers = new Map(); private _repositoryDisposables = new Map(); private readonly _disposables = new DisposableStore(); @@ -454,7 +490,8 @@ export class MainThreadSCM implements MainThreadSCMShape { @ITextModelService private readonly textModelService: ITextModelService, @IQuickDiffService private readonly quickDiffService: IQuickDiffService, @IUriIdentityService private readonly _uriIdentService: IUriIdentityService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @ILogService private readonly logService: ILogService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSCM); @@ -472,10 +509,10 @@ export class MainThreadSCM implements MainThreadSCMShape { } async $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): Promise { - // Eagerly create the text model for the input box - const inputBoxTextModelRef = await this.textModelService.createModelReference(URI.revive(inputBoxDocumentUri)); + this._repositoryBarriers.set(handle, new Barrier()); - const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, inputBoxTextModelRef.object.textEditorModel, this.quickDiffService, this._uriIdentService, this.workspaceContextService); + const inputBoxTextModelRef = await this.textModelService.createModelReference(URI.revive(inputBoxDocumentUri)); + const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, inputBoxTextModelRef.object.textEditorModel, this.quickDiffService, this._uriIdentService, this.workspaceContextService, this.logService); const repository = this.scmService.registerSCMProvider(provider); this._repositories.set(handle, repository); @@ -484,6 +521,7 @@ export class MainThreadSCM implements MainThreadSCMShape { Event.filter(this.scmViewService.onDidFocusRepository, r => r === repository)(_ => this._proxy.$setSelectedSourceControl(handle)), repository.input.onDidChange(({ value }) => this._proxy.$onInputBoxValueChange(handle, value)) ); + this._repositoryDisposables.set(handle, disposable); if (this.scmViewService.focusedRepository === repository) { setTimeout(() => this._proxy.$setSelectedSourceControl(handle), 0); @@ -493,10 +531,11 @@ export class MainThreadSCM implements MainThreadSCMShape { setTimeout(() => this._proxy.$onInputBoxValueChange(handle, repository.input.value), 0); } - this._repositoryDisposables.set(handle, disposable); + this._repositoryBarriers.get(handle)?.open(); } - $updateSourceControl(handle: number, features: SCMProviderFeatures): void { + async $updateSourceControl(handle: number, features: SCMProviderFeatures): Promise { + await this._repositoryBarriers.get(handle)?.wait(); const repository = this._repositories.get(handle); if (!repository) { @@ -507,7 +546,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$updateSourceControl(features); } - $unregisterSourceControl(handle: number): void { + async $unregisterSourceControl(handle: number): Promise { + await this._repositoryBarriers.get(handle)?.wait(); const repository = this._repositories.get(handle); if (!repository) { @@ -521,7 +561,8 @@ export class MainThreadSCM implements MainThreadSCMShape { this._repositories.delete(handle); } - $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures, /* multiDiffEditorEnableViewChanges */ boolean][], splices: SCMRawResourceSplices[]): void { + async $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures, /* multiDiffEditorEnableViewChanges */ boolean][], splices: SCMRawResourceSplices[]): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -533,7 +574,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$spliceGroupResourceStates(splices); } - $updateGroup(sourceControlHandle: number, groupHandle: number, features: SCMGroupFeatures): void { + async $updateGroup(sourceControlHandle: number, groupHandle: number, features: SCMGroupFeatures): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -544,7 +586,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$updateGroup(groupHandle, features); } - $updateGroupLabel(sourceControlHandle: number, groupHandle: number, label: string): void { + async $updateGroupLabel(sourceControlHandle: number, groupHandle: number, label: string): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -555,7 +598,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$updateGroupLabel(groupHandle, label); } - $spliceResourceStates(sourceControlHandle: number, splices: SCMRawResourceSplices[]): void { + async $spliceResourceStates(sourceControlHandle: number, splices: SCMRawResourceSplices[]): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -566,7 +610,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$spliceGroupResourceStates(splices); } - $unregisterGroup(sourceControlHandle: number, handle: number): void { + async $unregisterGroup(sourceControlHandle: number, handle: number): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -577,7 +622,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$unregisterGroup(handle); } - $setInputBoxValue(sourceControlHandle: number, value: string): void { + async $setInputBoxValue(sourceControlHandle: number, value: string): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -587,7 +633,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.setValue(value, false); } - $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void { + async $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -597,7 +644,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.placeholder = placeholder; } - $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): void { + async $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -607,7 +655,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.enabled = enabled; } - $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void { + async $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -617,7 +666,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.visible = visible; } - $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType) { + async $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { return; @@ -626,7 +676,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.showValidationMessage(message, type); } - $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void { + async $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -643,7 +694,8 @@ export class MainThreadSCM implements MainThreadSCMShape { } } - $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): void { + async $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index a3641b6687a..1b2a115fca2 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -150,7 +150,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh let value = task.coverage.read(undefined); if (!value) { value = new TestCoverage(run, taskId, this.uriIdentityService, { - getCoverageDetails: (id, token) => this.proxy.$getCoverageDetails(id, token) + getCoverageDetails: (id, testId, token) => this.proxy.$getCoverageDetails(id, testId, token) .then(r => r.map(CoverageDetails.deserialize)), }); value.append(deserialized, tx); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 680d04cb5dd..4b6a73823dc 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -14,7 +14,7 @@ import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions'; import { score, targetsNotebooks } from 'vs/editor/common/languageSelector'; import * as languageConfiguration from 'vs/editor/common/languages/languageConfiguration'; import { OverviewRulerLane } from 'vs/editor/common/model'; -import { ExtensionIdentifier, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as files from 'vs/platform/files/common/files'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILogService, ILoggerService, LogLevel } from 'vs/platform/log/common/log'; @@ -55,6 +55,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive'; import { ExtHostLabelService } from 'vs/workbench/api/common/extHostLabelService'; import { ExtHostLanguageFeatures } from 'vs/workbench/api/common/extHostLanguageFeatures'; +import { ExtHostLanguageModelTools } from 'vs/workbench/api/common/extHostLanguageModelTools'; import { IExtHostLanguageModels } from 'vs/workbench/api/common/extHostLanguageModels'; import { ExtHostLanguages } from 'vs/workbench/api/common/extHostLanguages'; import { IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService'; @@ -84,7 +85,7 @@ import { IExtHostTask } from 'vs/workbench/api/common/extHostTask'; import { ExtHostTelemetryLogger, IExtHostTelemetry, isNewAppInstall } from 'vs/workbench/api/common/extHostTelemetry'; import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { IExtHostTerminalShellIntegration } from 'vs/workbench/api/common/extHostTerminalShellIntegration'; -import { ExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; +import { IExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { ExtHostTimeline } from 'vs/workbench/api/common/extHostTimeline'; @@ -205,12 +206,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace)); const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); - const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol, extHostLogService, extHostCommands, extHostDocumentsAndEditors)); + const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, accessor.get(IExtHostTesting)); const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol)); const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService)); - const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, initData.quality)); + const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands)); const extHostChatVariables = rpcProtocol.set(ExtHostContext.ExtHostChatVariables, new ExtHostChatVariables(rpcProtocol)); + const extHostLanguageModelTools = rpcProtocol.set(ExtHostContext.ExtHostLanguageModelTools, new ExtHostLanguageModelTools(rpcProtocol)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); const extHostStatusBar = rpcProtocol.set(ExtHostContext.ExtHostStatusBar, new ExtHostStatusBar(rpcProtocol, extHostCommands.converter)); @@ -287,11 +289,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I if (typeof options?.forceNewSession === 'object' && options.forceNewSession.learnMore) { checkProposedApiEnabled(extension, 'authLearnMore'); } + if (options?.account) { + checkProposedApiEnabled(extension, 'authGetSessions'); + } return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, - getSessions(providerId: string, scopes: readonly string[]) { + getAccounts(providerId: string) { checkProposedApiEnabled(extension, 'authGetSessions'); - return extHostAuthentication.getSessions(extension, providerId, scopes); + return extHostAuthentication.getAccounts(providerId); }, // TODO: remove this after GHPR and Codespaces move off of it async hasSession(providerId: string, scopes: readonly string[]) { @@ -1430,28 +1435,20 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatParticipantPrivate'); return extHostChatAgents2.createDynamicChatAgent(extension, id, dynamicProps, handler); }, - attachContext(name: string, value: string | vscode.Uri | vscode.Location | unknown, location: vscode.ChatLocation.Panel) { - checkProposedApiEnabled(extension, 'chatVariableResolver'); - return extHostChatVariables.attachContext(name, value, location); - } }; // namespace: lm const lm: typeof vscode.lm = { selectChatModels: (selector) => { - if (initData.quality === 'stable') { - console.warn(`[${ExtensionIdentifier.toKey(extension.identifier)}] This API is disabled in '${initData.environment.appName}'-stable.`); - return Promise.resolve([]); - } return extHostLanguageModels.selectLanguageModels(extension, selector ?? {}); }, onDidChangeChatModels: (listener, thisArgs?, disposables?) => { - if (initData.quality === 'stable') { - console.warn(`[${ExtensionIdentifier.toKey(extension.identifier)}] This API is disabled in '${initData.environment.appName}'-stable.`); - return Event.None(listener, thisArgs, disposables); - } return extHostLanguageModels.onDidChangeProviders(listener, thisArgs, disposables); }, + registerChatModelProvider: (id, provider, metadata) => { + checkProposedApiEnabled(extension, 'chatProvider'); + return extHostLanguageModels.registerLanguageModel(extension, id, provider, metadata); + }, // --- embeddings get embeddingModels() { checkProposedApiEnabled(extension, 'embeddings'); @@ -1472,7 +1469,19 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } else { return extHostEmbeddings.computeEmbeddings(embeddingsModel, input, token); } - } + }, + registerTool(toolId: string, tool: vscode.LanguageModelTool) { + checkProposedApiEnabled(extension, 'lmTools'); + return extHostLanguageModelTools.registerTool(extension, toolId, tool); + }, + invokeTool(toolId: string, parameters: Object, token: vscode.CancellationToken) { + checkProposedApiEnabled(extension, 'lmTools'); + return extHostLanguageModelTools.invokeTool(toolId, parameters, token); + }, + get tools() { + checkProposedApiEnabled(extension, 'lmTools'); + return extHostLanguageModelTools.tools; + }, }; // namespace: speech @@ -1733,6 +1742,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatLocation: extHostTypes.ChatLocation, LanguageModelChatMessageRole: extHostTypes.LanguageModelChatMessageRole, LanguageModelChatMessage: extHostTypes.LanguageModelChatMessage, + LanguageModelChatMessageFunctionResultPart: extHostTypes.LanguageModelFunctionResultPart, + LanguageModelChatResponseTextPart: extHostTypes.LanguageModelTextPart, + LanguageModelChatResponseFunctionUsePart: extHostTypes.LanguageModelFunctionUsePart, LanguageModelChatMessage2: extHostTypes.LanguageModelChatMessage, // TODO@jrieken REMOVE LanguageModelChatSystemMessage: extHostTypes.LanguageModelChatSystemMessage,// TODO@jrieken REMOVE LanguageModelChatUserMessage: extHostTypes.LanguageModelChatUserMessage,// TODO@jrieken REMOVE diff --git a/src/vs/workbench/api/common/extHost.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index d01a3219f94..0427ebe7b17 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -31,6 +31,7 @@ import { ExtHostManagedSockets, IExtHostManagedSockets } from 'vs/workbench/api/ import { ExtHostAuthentication, IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { ExtHostLanguageModels, IExtHostLanguageModels } from 'vs/workbench/api/common/extHostLanguageModels'; import { IExtHostTerminalShellIntegration, ExtHostTerminalShellIntegration } from 'vs/workbench/api/common/extHostTerminalShellIntegration'; +import { ExtHostTesting, IExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; registerSingleton(IExtHostLocalizationService, ExtHostLocalizationService, InstantiationType.Delayed); registerSingleton(ILoggerService, ExtHostLoggerService, InstantiationType.Delayed); @@ -40,6 +41,7 @@ registerSingleton(IExtHostAuthentication, ExtHostAuthentication, InstantiationTy registerSingleton(IExtHostLanguageModels, ExtHostLanguageModels, InstantiationType.Eager); registerSingleton(IExtHostConfiguration, ExtHostConfiguration, InstantiationType.Eager); registerSingleton(IExtHostConsumerFileSystem, ExtHostConsumerFileSystem, InstantiationType.Eager); +registerSingleton(IExtHostTesting, ExtHostTesting, InstantiationType.Eager); registerSingleton(IExtHostDebugService, WorkerExtHostDebugService, InstantiationType.Eager); registerSingleton(IExtHostDecorations, ExtHostDecorations, InstantiationType.Eager); registerSingleton(IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors, InstantiationType.Eager); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 0c4b12e6412..c84af469d77 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -6,7 +6,6 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IRemoteConsoleLog } from 'vs/base/common/console'; -import { Location } from 'vs/editor/common/languages'; import { SerializedError } from 'vs/base/common/errors'; import { IRelativePattern } from 'vs/base/common/glob'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -54,9 +53,10 @@ import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/cal import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IToolData, IToolDelta } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from 'vs/workbench/contrib/chat/common/languageModels'; -import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; +import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; @@ -69,7 +69,7 @@ import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFil import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { RelatedInformationResult, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; -import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions } from 'vs/workbench/services/authentication/common/authentication'; +import { AuthenticationSession, AuthenticationSessionAccount, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProviderSessionOptions } from 'vs/workbench/services/authentication/common/authentication'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IExtensionDescriptionDelta, IStaticWorkspaceData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; @@ -144,10 +144,11 @@ export interface MainThreadCommentsShape extends IDisposable { $registerCommentController(handle: number, id: string, label: string, extensionId: string): void; $unregisterCommentController(handle: number): void; $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void; - $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, extensionId: ExtensionIdentifier, isTemplate: boolean, editorId?: string): languages.CommentThread | undefined; + $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, comments: languages.Comment[], extensionId: ExtensionIdentifier, isTemplate: boolean, editorId?: string): languages.CommentThread | undefined; $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, changes: CommentThreadChanges): void; $deleteCommentThread(handle: number, commentThreadHandle: number): void; $updateCommentingRanges(handle: number, resourceHints?: languages.CommentingRangeResourceHint): void; + $revealCommentThread(handle: number, commentThreadHandle: number, options: languages.CommentThreadRevealOptions): Promise; } export interface AuthenticationForceNewSessionOptions { @@ -161,7 +162,7 @@ export interface MainThreadAuthenticationShape extends IDisposable { $ensureProvider(id: string): Promise; $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void; $getSession(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string, options: { createIfNone?: boolean; forceNewSession?: boolean | AuthenticationForceNewSessionOptions; clearSessionPreference?: boolean }): Promise; - $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise; + $getAccounts(providerId: string): Promise>; $removeSession(providerId: string, sessionId: string): Promise; } @@ -1207,12 +1208,10 @@ export interface ExtHostSpeechShape { export interface MainThreadLanguageModelsShape extends IDisposable { $registerLanguageModelProvider(handle: number, identifier: string, metadata: ILanguageModelChatMetadata): void; $unregisterProvider(handle: number): void; - $handleProgressChunk(requestId: number, chunk: IChatResponseFragment): Promise; - + $tryStartChatRequest(extension: ExtensionIdentifier, provider: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise; + $reportResponsePart(requestId: number, chunk: IChatResponseFragment): Promise; + $reportResponseDone(requestId: number, error: SerializedError | undefined): Promise; $selectChatModels(selector: ILanguageModelChatSelector): Promise; - - $fetchResponse(extension: ExtensionIdentifier, provider: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise; - $whenLanguageModelChatRequestMade(identifier: string, extension: ExtensionIdentifier, participant?: string, tokenCount?: number): void; $countTokens(provider: string, value: string | IChatMessage, token: CancellationToken): Promise; } @@ -1220,8 +1219,9 @@ export interface MainThreadLanguageModelsShape extends IDisposable { export interface ExtHostLanguageModelsShape { $acceptChatModelMetadata(data: ILanguageModelsChangeEvent): void; $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void; - $provideLanguageModelResponse(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; - $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise; + $startChatRequest(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; + $acceptResponsePart(requestId: number, chunk: IChatResponseFragment): Promise; + $acceptResponseDone(requestId: number, error: SerializedError | undefined): Promise; $provideTokenLength(handle: number, value: string | IChatMessage, token: CancellationToken): Promise; } @@ -1298,7 +1298,13 @@ export interface MainThreadChatVariablesShape extends IDisposable { $registerVariable(handle: number, data: IChatVariableData): void; $handleProgressChunk(requestId: string, progress: IChatVariableResolverProgressDto): Promise; $unregisterVariable(handle: number): void; - $attachContext(name: string, value: string | Dto | URI | unknown, location: ChatAgentLocation): void; +} + +export interface MainThreadLanguageModelToolsShape extends IDisposable { + $getTools(): Promise; + $invokeTool(name: string, parameters: any, token: CancellationToken): Promise; + $registerTool(id: string): void; + $unregisterTool(name: string): void; } export type IChatRequestVariableValueDto = Dto; @@ -1307,6 +1313,11 @@ export interface ExtHostChatVariablesShape { $resolveVariable(handle: number, requestId: string, messageText: string, token: CancellationToken): Promise; } +export interface ExtHostLanguageModelToolsShape { + $acceptToolDelta(delta: IToolDelta): Promise; + $invokeTool(id: string, parameters: any, token: CancellationToken): Promise; +} + export interface MainThreadUrlsShape extends IDisposable { $registerUriHandler(handle: number, extensionId: ExtensionIdentifier, extensionDisplayName: string): Promise; $unregisterUriHandler(handle: number): Promise; @@ -1509,7 +1520,8 @@ export type SCMRawResourceSplices = [ export interface SCMHistoryItemGroupDto { readonly id: string; readonly name: string; - readonly base?: Omit; + readonly base?: Omit, 'remote'>; + readonly remote?: Omit, 'remote'>; } export interface SCMHistoryItemDto { @@ -1519,6 +1531,15 @@ export interface SCMHistoryItemDto { readonly author?: string; readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; readonly timestamp?: number; + readonly statistics?: { + readonly files: number; + readonly insertions: number; + readonly deletions: number; + }; + readonly labels?: { + readonly title: string; + readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; + }[]; } export interface SCMHistoryItemChangeDto { @@ -1530,24 +1551,24 @@ export interface SCMHistoryItemChangeDto { export interface MainThreadSCMShape extends IDisposable { $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): Promise; - $updateSourceControl(handle: number, features: SCMProviderFeatures): void; - $unregisterSourceControl(handle: number): void; + $updateSourceControl(handle: number, features: SCMProviderFeatures): Promise; + $unregisterSourceControl(handle: number): Promise; - $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures, /* multiDiffEditorEnableViewChanges */ boolean][], splices: SCMRawResourceSplices[]): void; - $updateGroup(sourceControlHandle: number, handle: number, features: SCMGroupFeatures): void; - $updateGroupLabel(sourceControlHandle: number, handle: number, label: string): void; - $unregisterGroup(sourceControlHandle: number, handle: number): void; + $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures, /* multiDiffEditorEnableViewChanges */ boolean][], splices: SCMRawResourceSplices[]): Promise; + $updateGroup(sourceControlHandle: number, handle: number, features: SCMGroupFeatures): Promise; + $updateGroupLabel(sourceControlHandle: number, handle: number, label: string): Promise; + $unregisterGroup(sourceControlHandle: number, handle: number): Promise; - $spliceResourceStates(sourceControlHandle: number, splices: SCMRawResourceSplices[]): void; + $spliceResourceStates(sourceControlHandle: number, splices: SCMRawResourceSplices[]): Promise; - $setInputBoxValue(sourceControlHandle: number, value: string): void; - $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void; - $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): void; - $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void; - $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): void; - $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void; + $setInputBoxValue(sourceControlHandle: number, value: string): Promise; + $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): Promise; + $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): Promise; + $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): Promise; + $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): Promise; + $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): Promise; - $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): void; + $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): Promise; } export interface MainThreadQuickDiffShape extends IDisposable { @@ -1574,6 +1595,7 @@ export interface IStartDebuggingOptions { suppressDebugStatusbar?: boolean; suppressDebugView?: boolean; suppressSaveBeforeStart?: boolean; + testRun?: IDebugTestRunReference; } export interface MainThreadDebugServiceShape extends IDisposable { @@ -1813,7 +1835,7 @@ export interface ExtHostLabelServiceShape { } export interface ExtHostAuthenticationShape { - $getSessions(id: string, scopes?: string[]): Promise>; + $getSessions(id: string, scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions): Promise>; $createSession(id: string, scopes: string[], options: IAuthenticationCreateSessionOptions): Promise; $removeSession(id: string, sessionId: string): Promise; $onDidChangeAuthenticationSessions(id: string, label: string): Promise; @@ -2173,6 +2195,7 @@ export interface ExtHostLanguageFeaturesShape { $resolveCompletionItem(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseCompletionItems(handle: number, id: number): void; $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise; + $provideInlineEdits(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise; $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void; $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void; $freeInlineCompletionsList(handle: number, pid: number): void; @@ -2202,6 +2225,7 @@ export interface ExtHostLanguageFeaturesShape { $provideTypeHierarchySubtypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $releaseTypeHierarchy(handle: number, sessionId: string): void; $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise; + $releaseDocumentOnDropEdits(handle: number, cacheId: number): void; $provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: IMappedEditsContextDto, token: CancellationToken): Promise; $provideInlineEdit(handle: number, document: UriComponents, context: languages.IInlineEditContext, token: CancellationToken): Promise; $freeInlineEdit(handle: number, pid: number): void; @@ -2313,6 +2337,7 @@ export interface ExtHostSCMShape { $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>; $setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise; $provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise; + $provideHistoryItems2(sourceControlHandle: number, options: any, token: CancellationToken): Promise; $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined>; @@ -2717,7 +2742,7 @@ export interface ExtHostTestingShape { /** Expands a test item's children, by the given number of levels. */ $expandTest(testId: string, levels: number): Promise; /** Requests coverage details for a test run. Errors if not available. */ - $getCoverageDetails(coverageId: string, token: CancellationToken): Promise; + $getCoverageDetails(coverageId: string, testId: string | undefined, token: CancellationToken): Promise; /** Disposes resources associated with a test run. */ $disposeRun(runId: string): void; /** Configures a test run config. */ @@ -2823,6 +2848,7 @@ export const MainContext = { MainThreadEmbeddings: createProxyIdentifier('MainThreadEmbeddings'), MainThreadChatAgents2: createProxyIdentifier('MainThreadChatAgents2'), MainThreadChatVariables: createProxyIdentifier('MainThreadChatVariables'), + MainThreadLanguageModelTools: createProxyIdentifier('MainThreadChatSkills'), MainThreadClipboard: createProxyIdentifier('MainThreadClipboard'), MainThreadCommands: createProxyIdentifier('MainThreadCommands'), MainThreadComments: createProxyIdentifier('MainThreadComments'), @@ -2942,6 +2968,7 @@ export const ExtHostContext = { ExtHostInteractive: createProxyIdentifier('ExtHostInteractive'), ExtHostChatAgents2: createProxyIdentifier('ExtHostChatAgents'), ExtHostChatVariables: createProxyIdentifier('ExtHostChatVariables'), + ExtHostLanguageModelTools: createProxyIdentifier('ExtHostChatSkills'), ExtHostChatProvider: createProxyIdentifier('ExtHostChatProvider'), ExtHostSpeech: createProxyIdentifier('ExtHostSpeech'), ExtHostEmbeddings: createProxyIdentifier('ExtHostEmbeddings'), diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 16b1d4a405b..c5ba402ef07 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -32,7 +32,6 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; private _getSessionTaskSingler = new TaskSingler(); - private _getSessionsTaskSingler = new TaskSingler>(); constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService @@ -54,14 +53,9 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { }); } - async getSessions(requestingExtension: IExtensionDescription, providerId: string, scopes: readonly string[]): Promise> { - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - const sortedScopes = [...scopes].sort().join(' '); - return await this._getSessionsTaskSingler.getOrCreate(`${extensionId} ${sortedScopes}`, async () => { - await this._proxy.$ensureProvider(providerId); - const extensionName = requestingExtension.displayName || requestingExtension.name; - return this._proxy.$getSessions(providerId, scopes, extensionId, extensionName); - }); + async getAccounts(providerId: string) { + await this._proxy.$ensureProvider(providerId); + return await this._proxy.$getAccounts(providerId); } async removeSession(providerId: string, sessionId: string): Promise { @@ -89,7 +83,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { }); } - async $createSession(providerId: string, scopes: string[], options: vscode.AuthenticationProviderCreateSessionOptions): Promise { + async $createSession(providerId: string, scopes: string[], options: vscode.AuthenticationProviderSessionOptions): Promise { const providerData = this._authenticationProviders.get(providerId); if (providerData) { return await providerData.provider.createSession(scopes, options); @@ -107,10 +101,10 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } - async $getSessions(providerId: string, scopes?: string[]): Promise> { + async $getSessions(providerId: string, scopes: ReadonlyArray | undefined, options: vscode.AuthenticationProviderSessionOptions): Promise> { const providerData = this._authenticationProviders.get(providerId); if (providerData) { - return await providerData.provider.getSessions(scopes); + return await providerData.provider.getSessions(scopes, options); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 9e8bd3e587c..37a81b0034d 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -22,7 +22,7 @@ import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extH import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatContentReference, IChatFollowup, IChatUserActionEvent, ChatAgentVoteDirection, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatResponseErrorDetails, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; @@ -263,7 +263,6 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS mainContext: IMainContext, private readonly _logService: ILogService, private readonly commands: ExtHostCommands, - private readonly quality: string | undefined ) { super(); this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2); @@ -275,19 +274,16 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS createChatAgent(extension: IExtensionDescription, id: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { const handle = ExtHostChatAgents2._idPool++; - const agent = new ExtHostChatAgent(extension, this.quality, id, this._proxy, handle, handler); + const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler); this._agents.set(handle, agent); - if (agent.isAgentEnabled()) { - this._proxy.$registerAgent(handle, extension.identifier, id, {}, undefined); - } - + this._proxy.$registerAgent(handle, extension.identifier, id, {}, undefined); return agent.apiAgent; } createDynamicChatAgent(extension: IExtensionDescription, id: string, dynamicProps: vscode.DynamicChatParticipantProps, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { const handle = ExtHostChatAgents2._idPool++; - const agent = new ExtHostChatAgent(extension, this.quality, id, this._proxy, handle, handler); + const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler); this._agents.set(handle, agent); this._proxy.$registerAgent(handle, extension.identifier, id, { isSticky: true } satisfies IExtensionChatAgentMetadata, dynamicProps); @@ -498,7 +494,6 @@ class ExtHostChatAgent { constructor( public readonly extension: IExtensionDescription, - private readonly quality: string | undefined, public readonly id: string, private readonly _proxy: MainThreadChatAgentsShape2, private readonly _handle: number, @@ -521,11 +516,6 @@ class ExtHostChatAgent { return await this._agentVariableProvider.provider.provideCompletionItems(query, token) ?? []; } - public isAgentEnabled() { - // If in stable and this extension doesn't have the right proposed API, then don't register the agent - return !(this.quality === 'stable' && !isProposedApiEnabled(this.extension, 'chatParticipantPrivate')); - } - async provideFollowups(result: vscode.ChatResult, context: vscode.ChatContext, token: CancellationToken): Promise { if (!this._followupProvider) { return []; @@ -583,10 +573,6 @@ class ExtHostChatAgent { } updateScheduled = true; queueMicrotask(() => { - if (!that.isAgentEnabled()) { - return; - } - this._proxy.$updateAgent(this._handle, { icon: !this._iconPath ? undefined : this._iconPath instanceof URI ? this._iconPath : diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts index 5f0bf7d2449..dfc37201bd4 100644 --- a/src/vs/workbench/api/common/extHostChatVariables.ts +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -64,10 +64,6 @@ export class ExtHostChatVariables implements ExtHostChatVariablesShape { this._proxy.$unregisterVariable(handle); }); } - - attachContext(name: string, value: string | vscode.Location | vscode.Uri | unknown, location: vscode.ChatLocation.Panel) { - this._proxy.$attachContext(name, extHostTypes.Location.isLocation(value) ? typeConvert.Location.from(value) : value, typeConvert.ChatLocation.from(location)); - } } class ChatVariableResolverResponseStream { diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index b3f54666152..c84fb4782f9 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -215,7 +215,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } else if (rangesResult) { ranges = { ranges: rangesResult.ranges || [], - fileComments: rangesResult.fileComments || false + fileComments: rangesResult.enableFileComments || false }; } else { ranges = rangesResult ?? undefined; @@ -424,6 +424,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo this._id, this._uri, extHostTypeConverter.Range.from(this._range), + this._comments.map(cmt => convertToDTOComment(this, cmt, this._commentsMap, this.extensionDescription)), extensionDescription.identifier, this._isTemplate, editorId @@ -436,9 +437,6 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo this.eventuallyUpdateCommentThread(); })); - // set up comments after ctor to batch update events. - this.comments = _comments; - this._localDisposables.push({ dispose: () => { proxy.$deleteCommentThread( @@ -465,6 +463,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo set label(value: string | undefined) { that.label = value; }, get state(): vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability } | undefined { return that.state; }, set state(value: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability }) { that.state = value; }, + reveal: (options?: vscode.CommentThreadRevealOptions) => that.reveal(options), dispose: () => { that.dispose(); } @@ -548,6 +547,11 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return; } + async reveal(options?: vscode.CommentThreadRevealOptions): Promise { + checkProposedApiEnabled(this.extensionDescription, 'commentReveal'); + return proxy.$revealCommentThread(this._commentControllerHandle, this.handle, { preserveFocus: false, focusReply: false, ...options }); + } + dispose() { this._isDiposed = true; this._acceptInputDisposables.dispose(); diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index d5ea2bdf817..a5e22aa3dee 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -7,7 +7,8 @@ import { asPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { Disposable as DisposableCls, toDisposable } from 'vs/base/common/lifecycle'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ISignService } from 'vs/platform/sign/common/sign'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -25,11 +26,11 @@ import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; import { IExtHostConfiguration } from '../common/extHostConfiguration'; import { IExtHostVariableResolverProvider } from './extHostVariableResolverService'; -import { toDisposable } from 'vs/base/common/lifecycle'; import { ThemeIcon as ThemeIconUtils } from 'vs/base/common/themables'; import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; import { coalesce } from 'vs/base/common/arrays'; +import { IExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; export const IExtHostDebugService = createDecorator('IExtHostDebugService'); @@ -60,7 +61,7 @@ export interface IExtHostDebugService extends ExtHostDebugServiceShape { asDebugSourceUri(source: vscode.DebugProtocolSource, session?: vscode.DebugSession): vscode.Uri; } -export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDebugServiceShape { +export abstract class ExtHostDebugServiceBase extends DisposableCls implements IExtHostDebugService, ExtHostDebugServiceShape { readonly _serviceBrand: undefined; @@ -123,7 +124,10 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E @IExtHostEditorTabs protected _editorTabs: IExtHostEditorTabs, @IExtHostVariableResolverProvider private _variableResolver: IExtHostVariableResolverProvider, @IExtHostCommands private _commands: IExtHostCommands, + @IExtHostTesting private _testing: IExtHostTesting, ) { + super(); + this._configProviderHandleCounter = 0; this._configProviders = []; @@ -136,25 +140,25 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E this._debugAdapters = new Map(); this._debugAdaptersTrackers = new Map(); - this._onDidStartDebugSession = new Emitter(); - this._onDidTerminateDebugSession = new Emitter(); - this._onDidChangeActiveDebugSession = new Emitter(); - this._onDidReceiveDebugSessionCustomEvent = new Emitter(); + this._onDidStartDebugSession = this._register(new Emitter()); + this._onDidTerminateDebugSession = this._register(new Emitter()); + this._onDidChangeActiveDebugSession = this._register(new Emitter()); + this._onDidReceiveDebugSessionCustomEvent = this._register(new Emitter()); this._debugServiceProxy = extHostRpcService.getProxy(MainContext.MainThreadDebugService); - this._onDidChangeBreakpoints = new Emitter(); + this._onDidChangeBreakpoints = this._register(new Emitter()); - this._onDidChangeActiveStackItem = new Emitter(); + this._onDidChangeActiveStackItem = this._register(new Emitter()); this._activeDebugConsole = new ExtHostDebugConsole(this._debugServiceProxy); this._breakpoints = new Map(); this._extensionService.getExtensionRegistry().then((extensionRegistry: ExtensionDescriptionRegistry) => { - extensionRegistry.onDidChange(_ => { + this._register(extensionRegistry.onDidChange(_ => { this.registerAllDebugTypes(extensionRegistry); - }); + })); this.registerAllDebugTypes(extensionRegistry); }); } @@ -169,7 +173,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E return item ? this.convertVisualizerTreeItem(treeId, item) : undefined; } - public registerDebugVisualizationTree(manifest: Readonly, id: string, provider: vscode.DebugVisualizationTree): vscode.Disposable { + public registerDebugVisualizationTree(manifest: IExtensionDescription, id: string, provider: vscode.DebugVisualizationTree): vscode.Disposable { const extensionId = ExtensionIdentifier.toKey(manifest.identifier); const key = this.extensionVisKey(extensionId, id); if (this._debugVisualizationProviders.has(key)) { @@ -464,6 +468,8 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E } public startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, options: vscode.DebugSessionOptions): Promise { + const testRunMeta = options.testRun && this._testing.getMetadataForRun(options.testRun); + return this._debugServiceProxy.$startDebugging(folder ? folder.uri : undefined, nameOrConfig, { parentSessionID: options.parentSession ? options.parentSession.id : undefined, lifecycleManagedByParent: options.lifecycleManagedByParent, @@ -471,6 +477,10 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E noDebug: options.noDebug, compact: options.compact, suppressSaveBeforeStart: options.suppressSaveBeforeStart, + testRun: testRunMeta && { + runId: testRunMeta.runId, + taskId: testRunMeta.taskId, + }, // Check debugUI for back-compat, #147264 suppressDebugStatusbar: options.suppressDebugStatusbar ?? (options as any).debugUI?.simple, @@ -1245,8 +1255,9 @@ export class WorkerExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostConfiguration configurationService: IExtHostConfiguration, @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider, - @IExtHostCommands commands: IExtHostCommands + @IExtHostCommands commands: IExtHostCommands, + @IExtHostTesting testing: IExtHostTesting, ) { - super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands, testing); } } diff --git a/src/vs/workbench/api/common/extHostDialogs.ts b/src/vs/workbench/api/common/extHostDialogs.ts index afc3abf5870..7518ee3ca19 100644 --- a/src/vs/workbench/api/common/extHostDialogs.ts +++ b/src/vs/workbench/api/common/extHostDialogs.ts @@ -7,7 +7,7 @@ import type * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { MainContext, MainThreadDiaglogsShape, IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; -import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export class ExtHostDialogs { @@ -17,7 +17,7 @@ export class ExtHostDialogs { this._proxy = mainContext.getProxy(MainContext.MainThreadDialogs); } - showOpenDialog(extension: IRelaxedExtensionDescription, options?: vscode.OpenDialogOptions): Promise { + showOpenDialog(extension: IExtensionDescription, options?: vscode.OpenDialogOptions): Promise { if (options?.allowUIResources) { checkProposedApiEnabled(extension, 'showLocal'); } diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 4ea250c3bf8..97935e89d08 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -24,7 +24,7 @@ import { MissingExtensionDependency, ActivationKind, checkProposedApiEnabled, is import { ExtensionDescriptionRegistry, IActivationEventsReader } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as errors from 'vs/base/common/errors'; import type * as vscode from 'vscode'; -import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { VSBuffer } from 'vs/base/common/buffer'; import { ExtensionGlobalMemento, ExtensionMemento } from 'vs/workbench/api/common/extHostMemento'; import { RemoteAuthorityResolverError, ExtensionKind, ExtensionMode, ExtensionRuntime, ManagedResolvedAuthority as ExtHostManagedResolvedAuthority } from 'vs/workbench/api/common/extHostTypes'; @@ -498,9 +498,10 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private _loadExtensionContext(extensionDescription: IExtensionDescription): Promise { const lanuageModelAccessInformation = this._extHostLanguageModels.createLanguageModelAccessInformation(extensionDescription); - const globalState = new ExtensionGlobalMemento(extensionDescription, this._storage); - const workspaceState = new ExtensionMemento(extensionDescription.identifier.value, false, this._storage); - const secrets = new ExtensionSecrets(extensionDescription, this._secretState); + // TODO: These should probably be disposed when the extension deactivates + const globalState = this._register(new ExtensionGlobalMemento(extensionDescription, this._storage)); + const workspaceState = this._register(new ExtensionMemento(extensionDescription.identifier.value, false, this._storage)); + const secrets = this._register(new ExtensionSecrets(extensionDescription, this._secretState)); const extensionMode = extensionDescription.isUnderDevelopment ? (this._initData.environment.extensionTestsLocationURI ? ExtensionMode.Test : ExtensionMode.Development) : ExtensionMode.Production; @@ -615,7 +616,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme }); } - private _activateAllStartupFinishedDeferred(extensions: Readonly[], start: number = 0): void { + private _activateAllStartupFinishedDeferred(extensions: IExtensionDescription[], start: number = 0): void { const timeBudget = 50; // 50 milliseconds const startTime = Date.now(); @@ -1230,7 +1231,7 @@ class SyncedActivationEventsReader implements IActivationEventsReader { this.addActivationEvents(activationEvents); } - public readActivationEvents(extensionDescription: Readonly): string[] { + public readActivationEvents(extensionDescription: IExtensionDescription): string[] { return this._map.get(extensionDescription.identifier) ?? []; } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 928021c186c..4483cd6d510 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -33,7 +33,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostTelemetry, IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import { CodeActionKind, CompletionList, Disposable, DocumentDropOrPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, NewSymbolNameTriggerKind, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from 'vs/workbench/api/common/extHostTypes'; -import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; import { Cache } from './cache'; import * as extHostProtocol from './extHost.protocol'; @@ -505,9 +505,9 @@ class CodeActionAdapter { } else { if (codeActionContext.only) { if (!candidate.kind) { - this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`); + this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value}' requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`); } else if (!codeActionContext.only.contains(candidate.kind)) { - this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`); + this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value}' requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`); } } @@ -1291,6 +1291,10 @@ class InlineCompletionAdapterBase { return undefined; } + async provideInlineEdits(resource: URI, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + return undefined; + } + disposeCompletions(pid: number): void { } handleDidShowCompletionItem(pid: number, idx: number, updatedInsertText: string): void { } @@ -1396,6 +1400,82 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { }; } + override async provideInlineEdits(resource: URI, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + if (!this._provider.provideInlineEdits) { + return undefined; + } + checkProposedApiEnabled(this._extension, 'inlineCompletionsAdditions'); + + const doc = this._documents.getDocument(resource); + const r = typeConvert.Range.to(range); + + const result = await this._provider.provideInlineEdits(doc, r, { + selectedCompletionInfo: + context.selectedSuggestionInfo + ? { + range: typeConvert.Range.to(context.selectedSuggestionInfo.range), + text: context.selectedSuggestionInfo.text + } + : undefined, + triggerKind: this.languageTriggerKindToVSCodeTriggerKind[context.triggerKind], + userPrompt: context.userPrompt, + }, token); + + if (!result) { + // undefined and null are valid results + return undefined; + } + + if (token.isCancellationRequested) { + // cancelled -> return without further ado, esp no caching + // of results as they will leak + return undefined; + } + + const normalizedResult = Array.isArray(result) ? result : result.items; + const commands = this._isAdditionsProposedApiEnabled ? Array.isArray(result) ? [] : result.commands || [] : []; + const enableForwardStability = this._isAdditionsProposedApiEnabled && !Array.isArray(result) ? result.enableForwardStability : undefined; + + let disposableStore: DisposableStore | undefined = undefined; + const pid = this._references.createReferenceId({ + dispose() { + disposableStore?.dispose(); + }, + items: normalizedResult + }); + + return { + pid, + items: normalizedResult.map((item, idx) => { + let command: languages.Command | undefined = undefined; + if (item.command) { + if (!disposableStore) { + disposableStore = new DisposableStore(); + } + command = this._commands.toInternal(item.command, disposableStore); + } + + const insertText = item.insertText; + return ({ + insertText: typeof insertText === 'string' ? insertText : { snippet: insertText.value }, + filterText: item.filterText, + range: item.range ? typeConvert.Range.from(item.range) : undefined, + command, + idx: idx, + completeBracketPairs: this._isAdditionsProposedApiEnabled ? item.completeBracketPairs : false, + }); + }), + commands: commands.map(c => { + if (!disposableStore) { + disposableStore = new DisposableStore(); + } + return this._commands.toInternal(c, disposableStore); + }), + suppressSuggestions: false, + enableForwardStability, + }; + } + override disposeCompletions(pid: number) { const data = this._references.disposeReferenceId(pid); data?.dispose(); @@ -2664,6 +2744,10 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, InlineCompletionAdapterBase, adapter => adapter.provideInlineCompletions(URI.revive(resource), position, context, token), undefined, token); } + $provideInlineEdits(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + return this._withAdapter(handle, InlineCompletionAdapterBase, adapter => adapter.provideInlineEdits(URI.revive(resource), range, context, token), undefined, token); + } + $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void { this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { adapter.handleDidShowCompletionItem(pid, idx, updatedInsertText); @@ -2912,7 +2996,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, DocumentDropEditAdapter, adapter => adapter.resolveDropEdit(id, token), {}, undefined); } - $releaseDropEdits(handle: number, cacheId: number): void { + $releaseDocumentOnDropEdits(handle: number, cacheId: number): void { this._withAdapter(handle, DocumentDropEditAdapter, adapter => Promise.resolve(adapter.releaseDropEdits(cacheId)), undefined, undefined); } diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts new file mode 100644 index 00000000000..e588f50ab6d --- /dev/null +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtHostLanguageModelToolsShape, IMainContext, MainContext, MainThreadLanguageModelToolsShape } from 'vs/workbench/api/common/extHost.protocol'; +import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; +import { IToolData, IToolDelta } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import type * as vscode from 'vscode'; + +export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape { + /** A map of tools that were registered in this EH */ + private readonly _registeredTools = new Map(); + private readonly _proxy: MainThreadLanguageModelToolsShape; + + /** A map of all known tools, from other EHs or registered in vscode core */ + private readonly _allTools = new Map(); + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.getProxy(MainContext.MainThreadLanguageModelTools); + + this._proxy.$getTools().then(tools => { + for (const tool of tools) { + this._allTools.set(tool.name, tool); + } + }); + } + + async invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + // Making the round trip here because not all tools were necessarily registered in this EH + return await this._proxy.$invokeTool(name, parameters, token); + } + + async $acceptToolDelta(delta: IToolDelta): Promise { + if (delta.added) { + this._allTools.set(delta.added.name, delta.added); + } + + if (delta.removed) { + this._allTools.delete(delta.removed); + } + } + + get tools(): vscode.LanguageModelToolDescription[] { + return Array.from(this._allTools.values()) + .map(tool => typeConvert.LanguageModelToolDescription.to(tool)); + } + + async $invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + const item = this._registeredTools.get(name); + if (!item) { + throw new Error(`Unknown tool ${name}`); + } + + return await item.tool.invoke(parameters, token); + } + + registerTool(extension: IExtensionDescription, name: string, tool: vscode.LanguageModelTool): IDisposable { + this._registeredTools.set(name, { extension, tool }); + this._proxy.$registerTool(name); + + return toDisposable(() => { + this._registeredTools.delete(name); + this._proxy.$unregisterTool(name); + }); + } +} diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index fdc93731554..b67999350af 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AsyncIterableSource, Barrier } from 'vs/base/common/async'; +import { AsyncIterableObject, AsyncIterableSource } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { CancellationError } from 'vs/base/common/errors'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { CancellationError, SerializedError, transformErrorForSerialization, transformErrorFromSerialization } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -19,7 +20,7 @@ import { IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentic import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IChatMessage, IChatResponseFragment, IChatResponsePart, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; import { INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; @@ -36,13 +37,13 @@ type LanguageModelData = { class LanguageModelResponseStream { - readonly stream = new AsyncIterableSource(); + readonly stream = new AsyncIterableSource(); constructor( readonly option: number, - stream?: AsyncIterableSource + stream?: AsyncIterableSource ) { - this.stream = stream ?? new AsyncIterableSource(); + this.stream = stream ?? new AsyncIterableSource(); } } @@ -51,17 +52,26 @@ class LanguageModelResponse { readonly apiObject: vscode.LanguageModelChatResponse; private readonly _responseStreams = new Map(); - private readonly _defaultStream = new AsyncIterableSource(); + private readonly _defaultStream = new AsyncIterableSource(); private _isDone: boolean = false; - private _isStreaming: boolean = false; constructor() { const that = this; this.apiObject = { // result: promise, - text: that._defaultStream.asyncIterable, - // streams: AsyncIterable[] // FUTURE responses per N + get stream() { + return that._defaultStream.asyncIterable; + }, + get text() { + return AsyncIterableObject.map(that._defaultStream.asyncIterable, part => { + if (part instanceof extHostTypes.LanguageModelTextPart) { + return part.value; + } else { + return undefined; + } + }).coalesce(); + }, }; } @@ -79,7 +89,6 @@ class LanguageModelResponse { if (this._isDone) { return; } - this._isStreaming = true; let res = this._responseStreams.get(fragment.index); if (!res) { if (this._responseStreams.size === 0) { @@ -90,13 +99,17 @@ class LanguageModelResponse { } this._responseStreams.set(fragment.index, res); } - res.stream.emitOne(fragment.part); - } - get isStreaming(): boolean { - return this._isStreaming; + let out: vscode.LanguageModelChatResponseTextPart | vscode.LanguageModelChatResponseFunctionUsePart; + if (fragment.part.type === 'text') { + out = new extHostTypes.LanguageModelTextPart(fragment.part.value); + } else { + out = new extHostTypes.LanguageModelFunctionUsePart(fragment.part.name, fragment.part.parameters); + } + res.stream.emitOne(out); } + reject(err: Error): void { this._isDone = true; for (const stream of this._streams()) { @@ -176,28 +189,65 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { }); } - async $provideLanguageModelResponse(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise { + async $startChatRequest(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: CancellationToken): Promise { const data = this._languageModels.get(handle); if (!data) { - return; + throw new Error('Provider not found'); } - const progress = new Progress(async fragment => { + const progress = new Progress(async fragment => { if (token.isCancellationRequested) { this._logService.warn(`[CHAT](${data.extension.value}) CANNOT send progress because the REQUEST IS CANCELLED`); return; } - this._proxy.$handleProgressChunk(requestId, { index: fragment.index, part: fragment.part }); + + let part: IChatResponsePart | undefined; + if (fragment.part instanceof extHostTypes.LanguageModelFunctionUsePart) { + part = { type: 'function_use', name: fragment.part.name, parameters: fragment.part.parameters }; + } else if (fragment.part instanceof extHostTypes.LanguageModelTextPart) { + part = { type: 'text', value: fragment.part.value }; + } + + if (!part) { + this._logService.warn(`[CHAT](${data.extension.value}) UNKNOWN part ${JSON.stringify(fragment)}`); + return; + } + + this._proxy.$reportResponsePart(requestId, { index: fragment.index, part }); }); - return data.provider.provideLanguageModelResponse( - messages.map(typeConvert.LanguageModelChatMessage.to), - options, - ExtensionIdentifier.toKey(from), - progress, - token - ); - } + let p: Promise; + + if (data.provider.provideLanguageModelResponse2) { + + p = Promise.resolve(data.provider.provideLanguageModelResponse2( + messages.map(typeConvert.LanguageModelChatMessage.to), + options, + ExtensionIdentifier.toKey(from), + progress, + token + )); + + } else { + + const progress2 = new Progress(async fragment => { + progress.report({ index: fragment.index, part: new extHostTypes.LanguageModelTextPart(fragment.part) }); + }); + + p = Promise.resolve(data.provider.provideLanguageModelResponse( + messages.map(typeConvert.LanguageModelChatMessage.to), + options?.modelOptions ?? {}, + ExtensionIdentifier.toKey(from), + progress2, + token + )); + } + p.then(() => { + this._proxy.$reportResponseDone(requestId, undefined); + }, err => { + this._proxy.$reportResponseDone(requestId, transformErrorForSerialization(err)); + }); + } //#region --- token counting @@ -311,45 +361,33 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } } - const requestId = (Math.random() * 1e6) | 0; - const requestPromise = this._proxy.$fetchResponse(from, languageModelId, requestId, internalMessages, options.modelOptions ?? {}, token); - - const barrier = new Barrier(); - - const res = new LanguageModelResponse(); - this._pendingRequest.set(requestId, { languageModelId, res }); + try { + const requestId = (Math.random() * 1e6) | 0; + const res = new LanguageModelResponse(); + this._pendingRequest.set(requestId, { languageModelId, res }); - let error: Error | undefined; + try { + await this._proxy.$tryStartChatRequest(from, languageModelId, requestId, internalMessages, options, token); - requestPromise.catch(err => { - if (barrier.isOpen()) { - // we received an error while streaming. this means we need to reject the "stream" - // because we have already returned the request object - res.reject(err); - } else { - error = err; + } catch (error) { + // error'ing here means that the request could NOT be started/made, e.g. wrong model, no access, etc, but + // later the response can fail as well. Those failures are communicated via the stream-object + this._pendingRequest.delete(requestId); + throw error; } - }).finally(() => { - this._pendingRequest.delete(requestId); - res.resolve(); - barrier.open(); - }); - await barrier.wait(); + return res.apiObject; - if (error) { + } catch (error) { if (error.name === extHostTypes.LanguageModelError.name) { throw error; } - throw new extHostTypes.LanguageModelError( - `Language model '${languageModelId}' errored, check cause for more details`, + `Language model '${languageModelId}' errored: ${toErrorMessage(error)}`, 'Unknown', error ); } - - return res.apiObject; } private _convertMessages(extension: IExtensionDescription, messages: vscode.LanguageModelChatMessage[]) { @@ -358,18 +396,36 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { if (message.role as number === extHostTypes.LanguageModelChatMessageRole.System) { checkProposedApiEnabled(extension, 'languageModelSystem'); } + if (message.content2 instanceof extHostTypes.LanguageModelFunctionResultPart) { + checkProposedApiEnabled(extension, 'lmTools'); + } internalMessages.push(typeConvert.LanguageModelChatMessage.from(message)); } return internalMessages; } - async $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise { - const data = this._pendingRequest.get(requestId);//.report(chunk); + async $acceptResponsePart(requestId: number, chunk: IChatResponseFragment): Promise { + const data = this._pendingRequest.get(requestId); if (data) { data.res.handleFragment(chunk); } } + async $acceptResponseDone(requestId: number, error: SerializedError | undefined): Promise { + const data = this._pendingRequest.get(requestId); + if (!data) { + return; + } + this._pendingRequest.delete(requestId); + if (error) { + // we error the stream because that's the only way to signal + // that the request has failed + data.res.reject(transformErrorFromSerialization(error)); + } else { + data.res.resolve(); + } + } + // BIG HACK: Using AuthenticationProviders to check access to Language Models private async _getAuthAccess(from: IExtensionDescription, to: { identifier: ExtensionIdentifier; displayName: string }, justification: string | undefined, silent: boolean | undefined): Promise { // This needs to be done in both MainThread & ExtHost ChatProvider diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 3a7e105c843..5b707fc865e 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -36,7 +36,7 @@ import { IExtHostSearch } from 'vs/workbench/api/common/extHostSearch'; import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchModel'; import { INotebookCellMatchNoModel, INotebookFileMatchNoModel, IRawClosedNotebookFileMatch, genericCellMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { NotebookPriorityInfo } from 'vs/workbench/contrib/search/common/search'; -import { globMatchesResource } from 'vs/workbench/services/editor/common/editorResolverService'; +import { globMatchesResource, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { ILogService } from 'vs/platform/log/common/log'; export class ExtHostNotebookController implements ExtHostNotebookShape { @@ -163,7 +163,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { providerDisplayName: extension.displayName || extension.name, displayName: registration.displayName, filenamePattern: viewOptionsFilenamePattern, - exclusive: registration.exclusive || false + priority: registration.exclusive ? RegisteredEditorPriority.exclusive : undefined }; } diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index c63a3afd172..6ce319f7334 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -9,7 +9,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { debounce } from 'vs/base/common/decorators'; import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { asPromise, Sequencer } from 'vs/base/common/async'; +import { asPromise } from 'vs/base/common/async'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto } from './extHost.protocol'; import { sortedDiff, equals } from 'vs/base/common/arrays'; @@ -58,19 +58,26 @@ function getIconResource(decorations?: vscode.SourceControlResourceThemableDecor } } -function getHistoryItemIconDto(historyItem: vscode.SourceControlHistoryItem): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined { - if (!historyItem.icon) { +function getHistoryItemIconDto(icon: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined { + if (!icon) { return undefined; - } else if (URI.isUri(historyItem.icon)) { - return historyItem.icon; - } else if (ThemeIcon.isThemeIcon(historyItem.icon)) { - return historyItem.icon; + } else if (URI.isUri(icon)) { + return icon; + } else if (ThemeIcon.isThemeIcon(icon)) { + return icon; } else { - const icon = historyItem.icon as { light: URI; dark: URI }; - return { light: icon.light, dark: icon.dark }; + const iconDto = icon as { light: URI; dark: URI }; + return { light: iconDto.light, dark: iconDto.dark }; } } +function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto { + const icon = getHistoryItemIconDto(historyItem.icon); + const labels = historyItem.labels?.map(l => ({ title: l.title, icon: getHistoryItemIconDto(l.icon) })); + + return { ...historyItem, icon, labels }; +} + function compareResourceThemableDecorations(a: vscode.SourceControlResourceThemableDecorations, b: vscode.SourceControlResourceThemableDecorations): number { if (!a.iconPath && !b.iconPath) { return 0; @@ -259,7 +266,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { set value(value: string) { value = value ?? ''; - this._sequencer.queue(async () => this.#proxy.$setInputBoxValue(this._sourceControlHandle, value)); + this.#proxy.$setInputBoxValue(this._sourceControlHandle, value); this.updateValue(value); } @@ -276,7 +283,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { } set placeholder(placeholder: string) { - this._sequencer.queue(async () => this.#proxy.$setInputBoxPlaceholder(this._sourceControlHandle, placeholder)); + this.#proxy.$setInputBoxPlaceholder(this._sourceControlHandle, placeholder); this._placeholder = placeholder; } @@ -296,7 +303,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { } this._validateInput = fn; - this._sequencer.queue(async () => this.#proxy.$setValidationProviderIsEnabled(this._sourceControlHandle, !!fn)); + this.#proxy.$setValidationProviderIsEnabled(this._sourceControlHandle, !!fn); } private _enabled: boolean = true; @@ -313,7 +320,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { } this._enabled = enabled; - this._sequencer.queue(async () => this.#proxy.$setInputBoxEnablement(this._sourceControlHandle, enabled)); + this.#proxy.$setInputBoxEnablement(this._sourceControlHandle, enabled); } private _visible: boolean = true; @@ -330,7 +337,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { } this._visible = visible; - this._sequencer.queue(async () => this.#proxy.$setInputBoxVisibility(this._sourceControlHandle, visible)); + this.#proxy.$setInputBoxVisibility(this._sourceControlHandle, visible); } get document(): vscode.TextDocument { @@ -339,7 +346,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { return this.#extHostDocuments.getDocument(this._documentUri); } - constructor(private _extension: IExtensionDescription, _extHostDocuments: ExtHostDocuments, proxy: MainThreadSCMShape, private _sequencer: Sequencer, private _sourceControlHandle: number, private _documentUri: URI) { + constructor(private _extension: IExtensionDescription, _extHostDocuments: ExtHostDocuments, proxy: MainThreadSCMShape, private _sourceControlHandle: number, private _documentUri: URI) { this.#extHostDocuments = _extHostDocuments; this.#proxy = proxy; } @@ -347,7 +354,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { showValidationMessage(message: string | vscode.MarkdownString, type: vscode.SourceControlInputBoxValidationType) { checkProposedApiEnabled(this._extension, 'scmValidation'); - this._sequencer.queue(async () => this.#proxy.$showValidationMessage(this._sourceControlHandle, message, type as any)); + this.#proxy.$showValidationMessage(this._sourceControlHandle, message, type as any); } $onInputBoxValueChange(value: string): void { @@ -386,14 +393,14 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG get label(): string { return this._label; } set label(label: string) { this._label = label; - this._sequencer.queue(async () => this._proxy.$updateGroupLabel(this._sourceControlHandle, this.handle, label)); + this._proxy.$updateGroupLabel(this._sourceControlHandle, this.handle, label); } private _hideWhenEmpty: boolean | undefined = undefined; get hideWhenEmpty(): boolean | undefined { return this._hideWhenEmpty; } set hideWhenEmpty(hideWhenEmpty: boolean | undefined) { this._hideWhenEmpty = hideWhenEmpty; - this._sequencer.queue(async () => this._proxy.$updateGroup(this._sourceControlHandle, this.handle, this.features)); + this._proxy.$updateGroup(this._sourceControlHandle, this.handle, this.features); } get features(): SCMGroupFeatures { @@ -413,7 +420,6 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG constructor( private _proxy: MainThreadSCMShape, private _commands: ExtHostCommands, - private _sequencer: Sequencer, private _sourceControlHandle: number, private _id: string, private _label: string, @@ -512,7 +518,6 @@ class ExtHostSourceControl implements vscode.SourceControl { #proxy: MainThreadSCMShape; - private readonly _sequencer = new Sequencer(); private _groups: Map = new Map(); get id(): string { @@ -542,7 +547,7 @@ class ExtHostSourceControl implements vscode.SourceControl { } this._count = count; - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { count })); + this.#proxy.$updateSourceControl(this.handle, { count }); } private _quickDiffProvider: vscode.QuickDiffProvider | undefined = undefined; @@ -557,7 +562,7 @@ class ExtHostSourceControl implements vscode.SourceControl { if (isProposedApiEnabled(this._extension, 'quickDiffProvider')) { quickDiffLabel = quickDiffProvider?.label; } - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { hasQuickDiffProvider: !!quickDiffProvider, quickDiffLabel })); + this.#proxy.$updateSourceControl(this.handle, { hasQuickDiffProvider: !!quickDiffProvider, quickDiffLabel }); } private _historyProvider: vscode.SourceControlHistoryProvider | undefined; @@ -575,12 +580,12 @@ class ExtHostSourceControl implements vscode.SourceControl { this._historyProvider = historyProvider; this._historyProviderDisposable.value = new DisposableStore(); - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { hasHistoryProvider: !!historyProvider })); + this.#proxy.$updateSourceControl(this.handle, { hasHistoryProvider: !!historyProvider }); if (historyProvider) { this._historyProviderDisposable.value.add(historyProvider.onDidChangeCurrentHistoryItemGroup(() => { this._historyProviderCurrentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; - this._sequencer.queue(async () => this.#proxy.$onDidChangeHistoryProviderCurrentHistoryItemGroup(this.handle, this._historyProviderCurrentHistoryItemGroup)); + this.#proxy.$onDidChangeHistoryProviderCurrentHistoryItemGroup(this.handle, this._historyProviderCurrentHistoryItemGroup); })); } } @@ -597,7 +602,7 @@ class ExtHostSourceControl implements vscode.SourceControl { } this._commitTemplate = commitTemplate; - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { commitTemplate })); + this.#proxy.$updateSourceControl(this.handle, { commitTemplate }); } private readonly _acceptInputDisposables = new MutableDisposable(); @@ -613,7 +618,7 @@ class ExtHostSourceControl implements vscode.SourceControl { this._acceptInputCommand = acceptInputCommand; const internal = this._commands.converter.toInternal(acceptInputCommand, this._acceptInputDisposables.value); - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { acceptInputCommand: internal })); + this.#proxy.$updateSourceControl(this.handle, { acceptInputCommand: internal }); } private readonly _actionButtonDisposables = new MutableDisposable(); @@ -637,7 +642,7 @@ class ExtHostSourceControl implements vscode.SourceControl { description: actionButton.description, enabled: actionButton.enabled } : undefined; - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { actionButton: internal ?? null })); + this.#proxy.$updateSourceControl(this.handle, { actionButton: internal ?? null }); } @@ -649,7 +654,9 @@ class ExtHostSourceControl implements vscode.SourceControl { } set statusBarCommands(statusBarCommands: vscode.Command[] | undefined) { + this.logService.trace('ExtHostSourceControl#statusBarCommands', (statusBarCommands ?? []).map(c => c.command).join(', ')); if (this._statusBarCommands && statusBarCommands && commandListEquals(this._statusBarCommands, statusBarCommands)) { + this.logService.trace('ExtHostSourceControl#statusBarCommands are equal'); return; } @@ -658,7 +665,7 @@ class ExtHostSourceControl implements vscode.SourceControl { this._statusBarCommands = statusBarCommands; const internal = (statusBarCommands || []).map(c => this._commands.converter.toInternal(c, this._statusBarDisposables.value!)) as ICommandDto[]; - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { statusBarCommands: internal })); + this.#proxy.$updateSourceControl(this.handle, { statusBarCommands: internal }); } private _selected: boolean = false; @@ -677,6 +684,7 @@ class ExtHostSourceControl implements vscode.SourceControl { _extHostDocuments: ExtHostDocuments, proxy: MainThreadSCMShape, private _commands: ExtHostCommands, + private readonly logService: ILogService, private _id: string, private _label: string, private _rootUri?: vscode.Uri @@ -689,8 +697,8 @@ class ExtHostSourceControl implements vscode.SourceControl { query: _rootUri ? `rootUri=${encodeURIComponent(_rootUri.toString())}` : undefined }); - this._sequencer.queue(() => this.#proxy.$registerSourceControl(this.handle, _id, _label, _rootUri, inputBoxDocumentUri)); - this._inputBox = new ExtHostSCMInputBox(_extension, _extHostDocuments, this.#proxy, this._sequencer, this.handle, inputBoxDocumentUri); + this._inputBox = new ExtHostSCMInputBox(_extension, _extHostDocuments, this.#proxy, this.handle, inputBoxDocumentUri); + this.#proxy.$registerSourceControl(this.handle, _id, _label, _rootUri, inputBoxDocumentUri); } private createdResourceGroups = new Map(); @@ -698,7 +706,7 @@ class ExtHostSourceControl implements vscode.SourceControl { createResourceGroup(id: string, label: string, options?: { multiDiffEditorEnableViewChanges?: boolean }): ExtHostSourceControlResourceGroup { const multiDiffEditorEnableViewChanges = isProposedApiEnabled(this._extension, 'scmMultiDiffEditor') && options?.multiDiffEditorEnableViewChanges === true; - const group = new ExtHostSourceControlResourceGroup(this.#proxy, this._commands, this._sequencer, this.handle, id, label, multiDiffEditorEnableViewChanges, this._extension); + const group = new ExtHostSourceControlResourceGroup(this.#proxy, this._commands, this.handle, id, label, multiDiffEditorEnableViewChanges, this._extension); const disposable = Event.once(group.onDidDispose)(() => this.createdResourceGroups.delete(group)); this.createdResourceGroups.set(group, disposable); this.eventuallyAddResourceGroups(); @@ -722,7 +730,7 @@ class ExtHostSourceControl implements vscode.SourceControl { this.updatedResourceGroups.delete(group); updateListener.dispose(); this._groups.delete(group.handle); - this._sequencer.queue(async () => this.#proxy.$unregisterGroup(this.handle, group.handle)); + this.#proxy.$unregisterGroup(this.handle, group.handle); }); groups.push([group.handle, group.id, group.label, group.features, group.multiDiffEditorEnableViewChanges]); @@ -736,7 +744,7 @@ class ExtHostSourceControl implements vscode.SourceControl { this._groups.set(group.handle, group); } - this._sequencer.queue(async () => this.#proxy.$registerGroups(this.handle, groups, splices)); + this.#proxy.$registerGroups(this.handle, groups, splices); this.createdResourceGroups.clear(); } @@ -755,7 +763,7 @@ class ExtHostSourceControl implements vscode.SourceControl { }); if (splices.length > 0) { - this._sequencer.queue(async () => this.#proxy.$spliceResourceStates(this.handle, splices)); + this.#proxy.$spliceResourceStates(this.handle, splices); } this.updatedResourceGroups.clear(); @@ -776,7 +784,7 @@ class ExtHostSourceControl implements vscode.SourceControl { this._statusBarDisposables.dispose(); this._groups.forEach(group => group.dispose()); - this._sequencer.queue(async () => this.#proxy.$unregisterSourceControl(this.handle)); + this.#proxy.$unregisterSourceControl(this.handle); } } @@ -856,7 +864,7 @@ export class ExtHostSCM implements ExtHostSCMShape { }); const handle = ExtHostSCM._handlePool++; - const sourceControl = new ExtHostSourceControl(extension, this._extHostDocuments, this._proxy, this._commands, id, label, rootUri); + const sourceControl = new ExtHostSourceControl(extension, this._extHostDocuments, this._proxy, this._commands, this.logService, id, label, rootUri); this._sourceControls.set(handle, sourceControl); const sourceControls = this._sourceControlsByExtension.get(extension.identifier) || []; @@ -971,7 +979,14 @@ export class ExtHostSCM implements ExtHostSCMShape { const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; const historyItems = await historyProvider?.provideHistoryItems(historyItemGroupId, options, token); - return historyItems?.map(item => ({ ...item, icon: getHistoryItemIconDto(item) })) ?? undefined; + return historyItems?.map(item => toSCMHistoryItemDto(item)) ?? undefined; + } + + async $provideHistoryItems2(sourceControlHandle: number, options: any, token: CancellationToken): Promise { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + const historyItems = await historyProvider?.provideHistoryItems2(options, token); + + return historyItems?.map(item => toSCMHistoryItemDto(item)) ?? undefined; } async $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise { @@ -981,7 +996,7 @@ export class ExtHostSCM implements ExtHostSCMShape { } const historyItem = await historyProvider.provideHistoryItemSummary(historyItemId, historyItemParentId, token); - return historyItem ? { ...historyItem, icon: getHistoryItemIconDto(historyItem) } : undefined; + return historyItem ? toSCMHistoryItemDto(historyItem) : undefined; } async $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise { diff --git a/src/vs/workbench/api/common/extHostSearch.ts b/src/vs/workbench/api/common/extHostSearch.ts index c2e0b93f7b9..f7da534aab8 100644 --- a/src/vs/workbench/api/common/extHostSearch.ts +++ b/src/vs/workbench/api/common/extHostSearch.ts @@ -25,7 +25,7 @@ export interface IExtHostSearch extends ExtHostSearchShape { export const IExtHostSearch = createDecorator('IExtHostSearch'); -export class ExtHostSearch implements ExtHostSearchShape { +export class ExtHostSearch implements IExtHostSearch { protected readonly _proxy: MainThreadSearchShape = this.extHostRpc.getProxy(MainContext.MainThreadSearch); protected _handlePool: number = 0; @@ -124,7 +124,7 @@ export class ExtHostSearch implements ExtHostSearchShape { $provideTextSearchResults(handle: number, session: number, rawQuery: IRawTextQuery, token: vscode.CancellationToken): Promise { const provider = this._textSearchProvider.get(handle); if (!provider || !provider.provideTextSearchResults) { - throw new Error(`2 Unknown provider ${handle}`); + throw new Error(`Unknown Text Search Provider ${handle}`); } const query = reviveQuery(rawQuery); @@ -135,7 +135,7 @@ export class ExtHostSearch implements ExtHostSearchShape { $provideAITextSearchResults(handle: number, session: number, rawQuery: IRawAITextQuery, token: vscode.CancellationToken): Promise { const provider = this._aiTextSearchProvider.get(handle); if (!provider || !provider.provideAITextSearchResults) { - throw new Error(`1 Unknown provider ${handle}`); + throw new Error(`Unknown AI Text Search Provider ${handle}`); } const query = reviveQuery(rawQuery); diff --git a/src/vs/workbench/api/common/extHostSecrets.ts b/src/vs/workbench/api/common/extHostSecrets.ts index d1af02ed1a2..13fb3293a35 100644 --- a/src/vs/workbench/api/common/extHostSecrets.ts +++ b/src/vs/workbench/api/common/extHostSecrets.ts @@ -9,26 +9,30 @@ import type * as vscode from 'vscode'; import { ExtHostSecretState } from 'vs/workbench/api/common/extHostSecretState'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; export class ExtensionSecrets implements vscode.SecretStorage { protected readonly _id: string; readonly #secretState: ExtHostSecretState; - private _onDidChange = new Emitter(); - readonly onDidChange: Event = this._onDidChange.event; - + readonly onDidChange: Event; + readonly disposables = new DisposableStore(); constructor(extensionDescription: IExtensionDescription, secretState: ExtHostSecretState) { this._id = ExtensionIdentifier.toKey(extensionDescription.identifier); this.#secretState = secretState; - this.#secretState.onDidChangePassword(e => { - if (e.extensionId === this._id) { - this._onDidChange.fire({ key: e.key }); - } - }); + this.onDidChange = Event.map( + Event.filter(this.#secretState.onDidChangePassword, e => e.extensionId === this._id), + e => ({ key: e.key }), + this.disposables + ); + } + + dispose() { + this.disposables.dispose(); } get(key: string): Promise { diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index b870c3ab033..9ce31e7147c 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -921,7 +921,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public getEnvironmentVariableCollection(extension: IExtensionDescription): IEnvironmentVariableCollection { let collection = this._environmentVariableCollections.get(extension.identifier.value); if (!collection) { - collection = new UnifiedEnvironmentVariableCollection(); + collection = this._register(new UnifiedEnvironmentVariableCollection()); this._setEnvironmentVariableCollection(extension.identifier.value, collection); } return collection.getScopedEnvironmentVariableCollection(undefined); @@ -936,7 +936,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public $initEnvironmentVariableCollections(collections: [string, ISerializableEnvironmentVariableCollection][]): void { collections.forEach(entry => { const extensionIdentifier = entry[0]; - const collection = new UnifiedEnvironmentVariableCollection(entry[1]); + const collection = this._register(new UnifiedEnvironmentVariableCollection(entry[1])); this._setEnvironmentVariableCollection(extensionIdentifier, collection); }); } @@ -952,20 +952,20 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I private _setEnvironmentVariableCollection(extensionIdentifier: string, collection: UnifiedEnvironmentVariableCollection): void { this._environmentVariableCollections.set(extensionIdentifier, collection); - collection.onDidChangeCollection(() => { + this._register(collection.onDidChangeCollection(() => { // When any collection value changes send this immediately, this is done to ensure // following calls to createTerminal will be created with the new environment. It will // result in more noise by sending multiple updates when called but collections are // expected to be small. this._syncEnvironmentVariableCollection(extensionIdentifier, collection); - }); + })); } } /** * Unified environment variable collection carrying information for all scopes, for a specific extension. */ -class UnifiedEnvironmentVariableCollection { +class UnifiedEnvironmentVariableCollection extends Disposable { readonly map: Map = new Map(); private readonly scopedCollections: Map = new Map(); readonly descriptionMap: Map = new Map(); @@ -983,6 +983,7 @@ class UnifiedEnvironmentVariableCollection { constructor( serialized?: ISerializableEnvironmentVariableCollection ) { + super(); this.map = new Map(serialized); } @@ -992,7 +993,7 @@ class UnifiedEnvironmentVariableCollection { if (!scopedCollection) { scopedCollection = new ScopedEnvironmentVariableCollection(this, scope); this.scopedCollections.set(scopedCollectionKey, scopedCollection); - scopedCollection.onDidChangeCollection(() => this._onDidChangeCollection.fire()); + this._register(scopedCollection.onDidChangeCollection(() => this._onDidChangeCollection.fire())); } return scopedCollection; } diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 64458c0b585..1ef9c81d439 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -15,17 +15,18 @@ import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecy import { MarshalledId } from 'vs/base/common/marshallingIds'; import { isDefined } from 'vs/base/common/types'; import { generateUuid } from 'vs/base/common/uuid'; -import { IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostTestingShape, ILocationDto, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostTestItemCollection, TestItemImpl, TestItemRootImpl, toItemFromContext } from 'vs/workbench/api/common/extHostTestItem'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { TestRunProfileKind, TestRunRequest, FileCoverage } from 'vs/workbench/api/common/extHostTypes'; +import { FileCoverage, TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extHostTypes'; import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; -import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; +import { TestId, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; @@ -35,7 +36,7 @@ interface ControllerInfo { controller: vscode.TestController; profiles: Map; collection: ExtHostTestItemCollection; - extension: Readonly; + extension: IExtensionDescription; activeProfiles: Set; } @@ -45,7 +46,14 @@ let followupCounter = 0; const testResultInternalIDs = new WeakMap(); +export const IExtHostTesting = createDecorator('IExtHostTesting'); +export interface IExtHostTesting extends ExtHostTesting { + readonly _serviceBrand: undefined; +} + export class ExtHostTesting extends Disposable implements ExtHostTestingShape { + declare readonly _serviceBrand: undefined; + private readonly resultsChangedEmitter = this._register(new Emitter()); protected readonly controllers = new Map(); private readonly proxy: MainThreadTestingShape; @@ -61,8 +69,8 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { constructor( @IExtHostRpcService rpc: IExtHostRpcService, @ILogService private readonly logService: ILogService, - private readonly commands: ExtHostCommands, - private readonly editors: ExtHostDocumentsAndEditors, + @IExtHostCommands private readonly commands: IExtHostCommands, + @IExtHostDocumentsAndEditors private readonly editors: IExtHostDocumentsAndEditors, ) { super(); this.proxy = rpc.getProxy(MainContext.MainThreadTesting); @@ -111,6 +119,8 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { }); } + //#region public API + /** * Implements vscode.test.registerTestProvider */ @@ -218,9 +228,9 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { await this.proxy.$runTests({ preserveFocus: req.preserveFocus ?? true, + group: profileGroupToBitset[profile.kind], targets: [{ testIds: req.include?.map(t => TestId.fromExtHostTestItem(t, controller.collection.root.id).toString()) ?? [controller.collection.root.id], - profileGroup: profileGroupToBitset[profile.kind], profileId: profile.profileId, controllerId: profile.controllerId, }], @@ -236,6 +246,9 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return { dispose: () => { this.followupProviders.delete(provider); } }; } + //#endregion + + //#region RPC methods /** * @inheritdoc */ @@ -250,8 +263,8 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { /** * @inheritdoc */ - async $getCoverageDetails(coverageId: string, token: CancellationToken): Promise { - const details = await this.runTracker.getCoverageDetails(coverageId, token); + async $getCoverageDetails(coverageId: string, testId: string | undefined, token: CancellationToken): Promise { + const details = await this.runTracker.getCoverageDetails(coverageId, testId, token); return details?.map(Convert.TestCoverage.fromDetails); } @@ -412,6 +425,30 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return this.commands.executeCommand(command.command, ...(command.arguments || [])); } + /** + * Cancels an ongoing test run. + */ + public $cancelExtensionTestRun(runId: string | undefined) { + if (runId === undefined) { + this.runTracker.cancelAllRuns(); + } else { + this.runTracker.cancelRunById(runId); + } + } + + //#endregion + + public getMetadataForRun(run: vscode.TestRun) { + for (const tracker of this.runTracker.trackers) { + const taskId = tracker.getTaskIdForRun(run); + if (taskId) { + return { taskId, runId: tracker.id }; + } + } + + return undefined; + } + private async runControllerTestRequest(req: ICallProfileRunHandler | ICallProfileRunHandler, isContinuous: boolean, token: CancellationToken): Promise { const lookup = this.controllers.get(req.controllerId); if (!lookup) { @@ -467,17 +504,6 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { } } } - - /** - * Cancels an ongoing test run. - */ - public $cancelExtensionTestRun(runId: string | undefined) { - if (runId === undefined) { - this.runTracker.cancelAllRuns(); - } else { - this.runTracker.cancelRunById(runId); - } - } } // Deadline after being requested by a user that a test run is forcibly cancelled. @@ -500,7 +526,7 @@ class TestRunTracker extends Disposable { private readonly cts: CancellationTokenSource; private readonly endEmitter = this._register(new Emitter()); private readonly onDidDispose: Event; - private readonly publishedCoverage = new Map(); + private readonly publishedCoverage = new Map(); /** * Fires when a test ends, and no more tests are left running. @@ -526,7 +552,7 @@ class TestRunTracker extends Disposable { private readonly proxy: MainThreadTestingShape, private readonly logService: ILogService, private readonly profile: vscode.TestRunProfile | undefined, - private readonly extension: IRelaxedExtensionDescription, + private readonly extension: IExtensionDescription, parentToken?: CancellationToken, ) { super(); @@ -543,6 +569,17 @@ class TestRunTracker extends Disposable { })); } + /** Gets the task ID from a test run object. */ + public getTaskIdForRun(run: vscode.TestRun) { + for (const [taskId, { run: r }] of this.tasks) { + if (r === run) { + return taskId; + } + } + + return undefined; + } + /** Requests cancellation of the run. On the second call, forces cancellation. */ public cancel() { if (this.state === TestRunTrackerState.Running) { @@ -554,19 +591,33 @@ class TestRunTracker extends Disposable { } /** Gets details for a previously-emitted coverage object. */ - public getCoverageDetails(id: string, token: CancellationToken) { + public async getCoverageDetails(id: string, testId: string | undefined, token: CancellationToken): Promise { const [, taskId] = TestId.fromString(id).path; /** runId, taskId, URI */ const coverage = this.publishedCoverage.get(id); if (!coverage) { return []; } + const { report, extIds } = coverage; const task = this.tasks.get(taskId); if (!task) { throw new Error('unreachable: run task was not found'); } - return this.profile?.loadDetailedCoverage?.(task.run, coverage, token) ?? []; + let testItem: vscode.TestItem | undefined; + if (testId && report instanceof FileCoverage) { + const index = extIds.indexOf(testId); + if (index === -1) { + return []; // ?? + } + testItem = report.fromTests[index]; + } + + const details = testItem + ? this.profile?.loadDetailedCoverageForTest?.(task.run, report, testItem, token) + : this.profile?.loadDetailedCoverage?.(task.run, report, token); + + return (await details) ?? []; } /** Creates the public test run interface to give to extensions. */ @@ -582,10 +633,6 @@ class TestRunTracker extends Disposable { return; } - if (!this.dto.isIncluded(test)) { - return; - } - this.ensureTestIsKnown(test); fn(test, ...args); }; @@ -610,7 +657,6 @@ class TestRunTracker extends Disposable { // one-off map used to associate test items with incrementing IDs in `addCoverage`. // There's no need to include their entire ID, we just want to make sure they're // stable and unique. Normal map is okay since TestRun lifetimes are limited. - const testItemCoverageId = new Map(); const run: vscode.TestRun = { isPersisted: this.dto.isPersisted, token: this.cts.token, @@ -621,28 +667,21 @@ class TestRunTracker extends Disposable { return; } - const testItem = coverage instanceof FileCoverage ? coverage.testItem : undefined; - let testItemIdPart: undefined | number; - if (testItem) { + const fromTests = coverage instanceof FileCoverage ? coverage.fromTests : []; + if (fromTests.length) { checkProposedApiEnabled(this.extension, 'attributableCoverage'); - if (!this.dto.isIncluded(testItem)) { - throw new Error('Attempted to `addCoverage` for a test item not included in the run'); - } - - this.ensureTestIsKnown(testItem); - testItemIdPart = testItemCoverageId.get(testItem); - if (testItemIdPart === undefined) { - testItemIdPart = testItemCoverageId.size; - testItemCoverageId.set(testItem, testItemIdPart); + for (const test of fromTests) { + this.ensureTestIsKnown(test); } } const uriStr = coverage.uri.toString(); - const id = new TestId(testItemIdPart !== undefined - ? [runId, taskId, uriStr, String(testItemIdPart)] - : [runId, taskId, uriStr], - ).toString(); - this.publishedCoverage.set(id, coverage); + const id = new TestId([runId, taskId, uriStr]).toString(); + // it's a lil funky, but it's possible for a test item's ID to change after + // it's been reported if it's rehomed under a different parent. Record its + // ID at the time when the coverage report is generated so we can reference + // it later if needeed. + this.publishedCoverage.set(id, { report: coverage, extIds: fromTests.map(t => TestId.fromExtHostTestItem(t, ctrlId).toString()) }); this.proxy.$appendCoverage(runId, taskId, Convert.TestCoverage.fromFile(ctrlId, id, coverage)); }, //#region state mutation @@ -673,11 +712,7 @@ class TestRunTracker extends Disposable { } if (test) { - if (this.dto.isIncluded(test)) { - this.ensureTestIsKnown(test); - } else { - test = undefined; - } + this.ensureTestIsKnown(test); } this.proxy.$appendOutputToRun( @@ -694,7 +729,6 @@ class TestRunTracker extends Disposable { } ended = true; - testItemCoverageId.clear(); this.proxy.$finishedTestRunTask(runId, taskId); if (!--this.running) { this.markEnded(); @@ -778,9 +812,9 @@ export class TestRunCoordinator { /** * Gets a coverage report for a given run and task ID. */ - public getCoverageDetails(id: string, token: vscode.CancellationToken) { + public getCoverageDetails(id: string, testId: string | undefined, token: vscode.CancellationToken) { const runId = TestId.root(id); - return this.trackedById.get(runId)?.getCoverageDetails(id, token) || []; + return this.trackedById.get(runId)?.getCoverageDetails(id, testId, token) || []; } /** @@ -802,7 +836,7 @@ export class TestRunCoordinator { * `$startedExtensionTestRun` is not invoked. The run must eventually * be cancelled manually. */ - public prepareForMainThreadTestRun(extension: IRelaxedExtensionDescription, req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile, token: CancellationToken) { + public prepareForMainThreadTestRun(extension: IExtensionDescription, req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile, token: CancellationToken) { return this.getTracker(req, dto, profile, extension, token); } @@ -825,7 +859,7 @@ export class TestRunCoordinator { /** * Implements the public `createTestRun` API. */ - public createTestRun(extension: IRelaxedExtensionDescription, controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { + public createTestRun(extension: IExtensionDescription, controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { const existing = this.tracked.get(request); if (existing) { return existing.createRun(name); @@ -854,7 +888,7 @@ export class TestRunCoordinator { return tracker.createRun(name); } - private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile | undefined, extension: IRelaxedExtensionDescription, token?: CancellationToken) { + private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile | undefined, extension: IExtensionDescription, token?: CancellationToken) { const tracker = new TestRunTracker(dto, this.proxy, this.logService, profile, extension, token); this.tracked.set(req, tracker); this.trackedById.set(tracker.id, tracker); @@ -875,15 +909,10 @@ const tryGetProfileFromTestRunReq = (request: vscode.TestRunRequest) => { }; export class TestRunDto { - private readonly includePrefix: string[]; - private readonly excludePrefix: string[]; - public static fromPublic(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, persist: boolean) { return new TestRunDto( controllerId, generateUuid(), - request.include?.map(t => TestId.fromExtHostTestItem(t, controllerId).toString()) ?? [controllerId], - request.exclude?.map(t => TestId.fromExtHostTestItem(t, controllerId).toString()) ?? [], persist, collection, ); @@ -893,8 +922,6 @@ export class TestRunDto { return new TestRunDto( request.controllerId, request.runId, - request.testIds, - request.excludeExtIds, true, collection, ); @@ -903,30 +930,9 @@ export class TestRunDto { constructor( public readonly controllerId: string, public readonly id: string, - include: string[], - exclude: string[], public readonly isPersisted: boolean, public readonly colllection: ExtHostTestItemCollection, ) { - this.includePrefix = include.map(id => id + TestIdPathParts.Delimiter); - this.excludePrefix = exclude.map(id => id + TestIdPathParts.Delimiter); - } - - public isIncluded(test: vscode.TestItem) { - const id = TestId.fromExtHostTestItem(test, this.controllerId).toString() + TestIdPathParts.Delimiter; - for (const prefix of this.excludePrefix) { - if (id === prefix || id.startsWith(prefix)) { - return false; - } - } - - for (const prefix of this.includePrefix) { - if (id === prefix || id.startsWith(prefix)) { - return true; - } - } - - return false; } } diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index 7ab8d65df53..277422f9acb 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -5,6 +5,7 @@ import * as arrays from 'vs/base/common/arrays'; import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; @@ -13,7 +14,7 @@ import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; import * as vscode from 'vscode'; -export class ExtHostEditors implements ExtHostEditorsShape { +export class ExtHostEditors extends Disposable implements ExtHostEditorsShape { private readonly _onDidChangeTextEditorSelection = new Emitter(); private readonly _onDidChangeTextEditorOptions = new Emitter(); @@ -35,11 +36,11 @@ export class ExtHostEditors implements ExtHostEditorsShape { mainContext: IMainContext, private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, ) { + super(); this._proxy = mainContext.getProxy(MainContext.MainThreadTextEditors); - - this._extHostDocumentsAndEditors.onDidChangeVisibleTextEditors(e => this._onDidChangeVisibleTextEditors.fire(e)); - this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(e => this._onDidChangeActiveTextEditor.fire(e)); + this._register(this._extHostDocumentsAndEditors.onDidChangeVisibleTextEditors(e => this._onDidChangeVisibleTextEditors.fire(e))); + this._register(this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(e => this._onDidChangeActiveTextEditor.fire(e))); } getActiveTextEditor(): vscode.TextEditor | undefined { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6525d0f2009..32d972e9862 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -53,6 +53,7 @@ import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/ed import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; import * as types from './extHostTypes'; +import { IToolData } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; export namespace Command { @@ -1330,7 +1331,9 @@ export namespace DocumentLink { // ignore } } - return new types.DocumentLink(Range.to(link.range), target); + const result = new types.DocumentLink(Range.to(link.range), target); + result.tooltip = link.tooltip; + return result; } } @@ -2063,8 +2066,8 @@ export namespace TestCoverage { statement: fromCoverageCount(coverage.statementCoverage), branch: coverage.branchCoverage && fromCoverageCount(coverage.branchCoverage), declaration: coverage.declarationCoverage && fromCoverageCount(coverage.declarationCoverage), - testId: coverage instanceof types.FileCoverage && coverage.testItem ? - TestId.fromExtHostTestItem(coverage.testItem, controllerId).toString() : undefined, + testIds: coverage instanceof types.FileCoverage && coverage.fromTests.length ? + coverage.fromTests.map(t => TestId.fromExtHostTestItem(t, controllerId).toString()) : undefined, }; } } @@ -2241,23 +2244,69 @@ export namespace ChatFollowup { } } +export namespace LanguageModelChatMessageRole { + export function to(role: chatProvider.ChatMessageRole): vscode.LanguageModelChatMessageRole { + switch (role) { + case chatProvider.ChatMessageRole.System: return types.LanguageModelChatMessageRole.System; + case chatProvider.ChatMessageRole.User: return types.LanguageModelChatMessageRole.User; + case chatProvider.ChatMessageRole.Assistant: return types.LanguageModelChatMessageRole.Assistant; + } + } + + export function from(role: vscode.LanguageModelChatMessageRole): chatProvider.ChatMessageRole { + switch (role) { + case types.LanguageModelChatMessageRole.System: return chatProvider.ChatMessageRole.System; + case types.LanguageModelChatMessageRole.User: return chatProvider.ChatMessageRole.User; + case types.LanguageModelChatMessageRole.Assistant: return chatProvider.ChatMessageRole.Assistant; + } + return chatProvider.ChatMessageRole.User; + } +} export namespace LanguageModelChatMessage { export function to(message: chatProvider.IChatMessage): vscode.LanguageModelChatMessage { - switch (message.role) { - case chatProvider.ChatMessageRole.System: return new types.LanguageModelChatMessage(types.LanguageModelChatMessageRole.System, message.content); - case chatProvider.ChatMessageRole.User: return new types.LanguageModelChatMessage(types.LanguageModelChatMessageRole.User, message.content); - case chatProvider.ChatMessageRole.Assistant: return new types.LanguageModelChatMessage(types.LanguageModelChatMessageRole.Assistant, message.content); + let content: string = ''; + let content2: vscode.LanguageModelChatMessageFunctionResultPart | undefined; + if (message.content.type === 'text') { + content = message.content.value; + } else { + content2 = new types.LanguageModelFunctionResultPart(message.content.name, message.content.value, message.content.isError); + } + const role = LanguageModelChatMessageRole.to(message.role); + const result = new types.LanguageModelChatMessage(role, content, message.name); + if (content2 !== undefined) { + result.content2 = content2; } + return result; } export function from(message: vscode.LanguageModelChatMessage): chatProvider.IChatMessage { - switch (message.role as types.LanguageModelChatMessageRole) { - case types.LanguageModelChatMessageRole.System: return { role: chatProvider.ChatMessageRole.System, content: message.content }; - case types.LanguageModelChatMessageRole.User: return { role: chatProvider.ChatMessageRole.User, content: message.content }; - case types.LanguageModelChatMessageRole.Assistant: return { role: chatProvider.ChatMessageRole.Assistant, content: message.content }; + + const role = LanguageModelChatMessageRole.from(message.role); + const name = message.name; + + let content: chatProvider.IChatMessagePart; + + if (message.content2 instanceof types.LanguageModelFunctionResultPart) { + content = { + type: 'function_result', + name: message.content2.name, + value: message.content2.content, + isError: message.content2.isError + }; + } else { + content = { + type: 'text', + value: message.content + }; } + + return { + role, + name, + content + }; } } @@ -2695,3 +2744,13 @@ export namespace DebugTreeItem { }; } } + +export namespace LanguageModelToolDescription { + export function to(item: IToolData): vscode.LanguageModelToolDescription { + return { + name: item.name, + description: item.description, + parametersSchema: item.parametersSchema, + }; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index d55dc62ec74..76a834a9c89 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4119,7 +4119,7 @@ export class FileCoverage implements vscode.FileCoverage { public statementCoverage: vscode.TestCoverageCount, public branchCoverage?: vscode.TestCoverageCount, public declarationCoverage?: vscode.TestCoverageCount, - public testItem?: vscode.TestItem, + public fromTests: vscode.TestItem[] = [], ) { } } @@ -4563,10 +4563,25 @@ export enum LanguageModelChatMessageRole { System = 3 } +export class LanguageModelFunctionResultPart implements vscode.LanguageModelChatMessageFunctionResultPart { + + name: string; + content: string; + isError: boolean; + + constructor(name: string, content: string, isError?: boolean) { + this.name = name; + this.content = content; + this.isError = isError ?? false; + } +} + export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage { - static User(content: string, name?: string): LanguageModelChatMessage { - return new LanguageModelChatMessage(LanguageModelChatMessageRole.User, content, name); + static User(content: string | LanguageModelFunctionResultPart, name?: string): LanguageModelChatMessage { + const value = new LanguageModelChatMessage(LanguageModelChatMessageRole.User, typeof content === 'string' ? content : '', name); + value.content2 = content; + return value; } static Assistant(content: string, name?: string): LanguageModelChatMessage { @@ -4575,15 +4590,36 @@ export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage role: vscode.LanguageModelChatMessageRole; content: string; + content2: string | vscode.LanguageModelChatMessageFunctionResultPart; name: string | undefined; constructor(role: vscode.LanguageModelChatMessageRole, content: string, name?: string) { this.role = role; this.content = content; + this.content2 = content; this.name = name; } } +export class LanguageModelFunctionUsePart implements vscode.LanguageModelChatResponseFunctionUsePart { + name: string; + parameters: any; + + constructor(name: string, parameters: any) { + this.name = name; + this.parameters = parameters; + } +} + +export class LanguageModelTextPart implements vscode.LanguageModelChatResponseTextPart { + value: string; + + constructor(value: string) { + this.value = value; + + } +} + /** * @deprecated */ diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 08b842e84fb..5b3cd328aaa 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -132,7 +132,7 @@ class ExtHostWorkspaceImpl extends Workspace { constructor(id: string, private _name: string, folders: vscode.WorkspaceFolder[], transient: boolean, configuration: URI | null, private _isUntitled: boolean, ignorePathCasing: (key: URI) => boolean) { super(id, folders.map(f => new WorkspaceFolder(f)), transient, configuration, ignorePathCasing); - this._structure = TernarySearchTree.forUris(ignorePathCasing); + this._structure = TernarySearchTree.forUris(ignorePathCasing, () => true); // setup the workspace folder data structure folders.forEach(folder => { diff --git a/src/vs/workbench/api/common/extensionHostMain.ts b/src/vs/workbench/api/common/extensionHostMain.ts index 2bd275cbc21..50c47ba98a8 100644 --- a/src/vs/workbench/api/common/extensionHostMain.ts +++ b/src/vs/workbench/api/common/extensionHostMain.ts @@ -11,7 +11,7 @@ import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; import { IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { RPCProtocol } from 'vs/workbench/services/extensions/common/rpcProtocol'; -import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { getSingletonServiceDescriptors } from 'vs/platform/instantiation/common/extensions'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -195,7 +195,7 @@ export class ExtensionHostMain { private static _transform(initData: IExtensionHostInitData, rpcProtocol: RPCProtocol): IExtensionHostInitData { initData.extensions.allExtensions.forEach((ext) => { - (>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation)); + (>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation)); }); initData.environment.appRoot = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appRoot)); const extDevLocs = initData.environment.extensionDevelopmentLocationURI; diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index fd03fd9ea32..dc48354e372 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -27,6 +27,7 @@ import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/c import type * as vscode from 'vscode'; import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { IExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; export class ExtHostDebugService extends ExtHostDebugServiceBase { @@ -44,8 +45,9 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider, @IExtHostCommands commands: IExtHostCommands, + @IExtHostTesting testing: IExtHostTesting, ) { - super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands, testing); } protected override createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { @@ -78,9 +80,9 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { if (!this._terminalDisposedListener) { // React on terminal disposed and check if that is the debug terminal #12956 - this._terminalDisposedListener = this._terminalService.onDidCloseTerminal(terminal => { + this._terminalDisposedListener = this._register(this._terminalService.onDidCloseTerminal(terminal => { this._integratedTerminalInstances.onTerminalClosed(terminal); - }); + })); } const configProvider = await this._configurationService.getConfigProvider(); diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index 99f5d7614e8..d10dbbb7d99 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -8,6 +8,7 @@ import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import * as pfs from 'vs/base/node/pfs'; import { ILogService } from 'vs/platform/log/common/log'; +import { IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostSearch, reviveQuery } from 'vs/workbench/api/common/extHostSearch'; @@ -29,24 +30,59 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { private _registeredEHSearchProvider = false; + private _numThreadsPromise: Promise | undefined; + private readonly _disposables = new DisposableStore(); + private isDisposed = false; + constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @IExtHostInitDataService initData: IExtHostInitDataService, @IURITransformerService _uriTransformer: IURITransformerService, + @IExtHostConfiguration private readonly configurationService: IExtHostConfiguration, @ILogService _logService: ILogService, ) { super(extHostRpc, _uriTransformer, _logService); - + this.getNumThreads = this.getNumThreads.bind(this); + this.getNumThreadsCached = this.getNumThreadsCached.bind(this); + this.handleConfigurationChanged = this.handleConfigurationChanged.bind(this); const outputChannel = new OutputChannel('RipgrepSearchUD', this._logService); - this._disposables.add(this.registerTextSearchProvider(Schemas.vscodeUserData, new RipgrepSearchProvider(outputChannel))); + this._disposables.add(this.registerTextSearchProvider(Schemas.vscodeUserData, new RipgrepSearchProvider(outputChannel, this.getNumThreadsCached))); if (initData.remote.isRemote && initData.remote.authority) { this._registerEHSearchProviders(); } + + configurationService.getConfigProvider().then(provider => { + if (this.isDisposed) { + return; + } + this._disposables.add(provider.onDidChangeConfiguration(this.handleConfigurationChanged)); + }); + } + + private handleConfigurationChanged(event: vscode.ConfigurationChangeEvent) { + if (!event.affectsConfiguration('search')) { + return; + } + this._numThreadsPromise = undefined; + } + + async getNumThreads(): Promise { + const configProvider = await this.configurationService.getConfigProvider(); + const numThreads = configProvider.getConfiguration('search').get('ripgrep.maxThreads'); + return numThreads; + } + + async getNumThreadsCached(): Promise { + if (!this._numThreadsPromise) { + this._numThreadsPromise = this.getNumThreads(); + } + return this._numThreadsPromise; } dispose(): void { + this.isDisposed = true; this._disposables.dispose(); } @@ -61,8 +97,8 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { this._registeredEHSearchProvider = true; const outputChannel = new OutputChannel('RipgrepSearchEH', this._logService); - this._disposables.add(this.registerTextSearchProvider(Schemas.file, new RipgrepSearchProvider(outputChannel))); - this._disposables.add(this.registerInternalFileSearchProvider(Schemas.file, new SearchService('fileSearchProvider'))); + this._disposables.add(this.registerTextSearchProvider(Schemas.file, new RipgrepSearchProvider(outputChannel, this.getNumThreadsCached))); + this._disposables.add(this.registerInternalFileSearchProvider(Schemas.file, new SearchService('fileSearchProvider', this.getNumThreadsCached))); } private registerInternalFileSearchProvider(scheme: string, provider: SearchService): IDisposable { @@ -90,7 +126,7 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { return super.$provideFileSearchResults(handle, session, rawQuery, token); } - override doInternalFileSearchWithCustomCallback(rawQuery: IFileQuery, token: vscode.CancellationToken, handleFileMatch: (data: URI[]) => void): Promise { + override async doInternalFileSearchWithCustomCallback(rawQuery: IFileQuery, token: vscode.CancellationToken, handleFileMatch: (data: URI[]) => void): Promise { const onResult = (ev: ISerializedSearchProgressItem) => { if (isSerializedFileMatch(ev)) { ev = [ev]; @@ -109,8 +145,8 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { if (!this._internalFileSearchProvider) { throw new Error('No internal file search handler'); } - - return >this._internalFileSearchProvider.doFileSearch(rawQuery, onResult, token); + const numThreads = await this.getNumThreadsCached(); + return >this._internalFileSearchProvider.doFileSearch(rawQuery, numThreads, onResult, token); } private async doInternalFileSearch(handle: number, session: number, rawQuery: IFileQuery, token: vscode.CancellationToken): Promise { diff --git a/src/vs/workbench/api/node/extensionHostProcess.ts b/src/vs/workbench/api/node/extensionHostProcess.ts index 785db7edd43..80a60c1d4c1 100644 --- a/src/vs/workbench/api/node/extensionHostProcess.ts +++ b/src/vs/workbench/api/node/extensionHostProcess.ts @@ -3,29 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import minimist from 'minimist'; import * as nativeWatchdog from 'native-watchdog'; import * as net from 'net'; -import * as minimist from 'minimist'; -import * as performance from 'vs/base/common/performance'; -import type { MessagePortMain } from 'vs/base/parts/sandbox/node/electronTypes'; +import { ProcessTimeRunOnceScheduler } from 'vs/base/common/async'; +import { VSBuffer } from 'vs/base/common/buffer'; import { isCancellationError, isSigPipeError, onUnexpectedError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; +import * as performance from 'vs/base/common/performance'; +import { IURITransformer } from 'vs/base/common/uriIpc'; +import { realpath } from 'vs/base/node/extpath'; +import { Promises } from 'vs/base/node/pfs'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { PersistentProtocol, ProtocolConstants, BufferedEmitter } from 'vs/base/parts/ipc/common/ipc.net'; +import { BufferedEmitter, PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import type { MessagePortMain } from 'vs/base/parts/sandbox/node/electronTypes'; +import { boolean } from 'vs/editor/common/config/editorOptions'; import product from 'vs/platform/product/common/product'; -import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, ExtensionHostExitCode, IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtensionHostMain, IExitFn } from 'vs/workbench/api/common/extensionHostMain'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { IURITransformer } from 'vs/base/common/uriIpc'; -import { Promises } from 'vs/base/node/pfs'; -import { realpath } from 'vs/base/node/extpath'; import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; -import { ProcessTimeRunOnceScheduler } from 'vs/base/common/async'; -import { boolean } from 'vs/editor/common/config/editorOptions'; import { createURITransformer } from 'vs/workbench/api/node/uriTransformer'; import { ExtHostConnectionType, readExtHostConnection } from 'vs/workbench/services/extensions/common/extensionHostEnv'; +import { ExtensionHostExitCode, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, IExtHostSocketMessage, IExtensionHostInitData, MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IDisposable } from 'vs/base/common/lifecycle'; import 'vs/workbench/api/common/extHost.common.services'; import 'vs/workbench/api/node/extHost.node.services'; @@ -251,12 +252,14 @@ async function createExtHostProtocol(): Promise { readonly onMessage: Event = this._onMessage.event; private _terminating: boolean; + private _protocolListener: IDisposable; constructor() { this._terminating = false; - protocol.onMessage((msg) => { + this._protocolListener = protocol.onMessage((msg) => { if (isMessageOfType(msg, MessageType.Terminate)) { this._terminating = true; + this._protocolListener.dispose(); onTerminate('received terminate message from renderer'); } else { this._onMessage.fire(msg); diff --git a/src/vs/workbench/api/node/proxyResolver.ts b/src/vs/workbench/api/node/proxyResolver.ts index 519924eec13..ac373f9739b 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -35,6 +35,7 @@ export function connectProxyResolver( lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostLogService, mainThreadTelemetry, configProvider, {}, initData.remote.isRemote), getProxyURL: () => configProvider.getConfiguration('http').get('proxy'), getProxySupport: () => configProvider.getConfiguration('http').get('proxySupport') || 'off', + getNoProxyConfig: () => configProvider.getConfiguration('http').get('noProxy') || [], addCertificatesV1: () => certSettingV1(configProvider), addCertificatesV2: () => certSettingV2(configProvider), log: extHostLogService, @@ -67,6 +68,11 @@ export function connectProxyResolver( certs.then(certs => extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loaded certificates from main process', certs.length)); promises.push(certs); } + // Using https.globalAgent because it is shared with proxy.test.ts and mutable. + if (initData.environment.extensionTestsLocationURI && (https.globalAgent as any).testCertificates?.length) { + extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loading test certificates'); + promises.push(Promise.resolve((https.globalAgent as any).testCertificates as string[])); + } return (await Promise.all(promises)).flat(); }, env: process.env, @@ -77,11 +83,16 @@ export function connectProxyResolver( } function createPatchedModules(params: ProxyAgentParams, resolveProxy: ReturnType) { + + function mergeModules(module: any, patch: any) { + return Object.assign(module.default || module, patch); + } + return { - http: Object.assign(http, createHttpPatch(params, http, resolveProxy)), - https: Object.assign(https, createHttpPatch(params, https, resolveProxy)), - net: Object.assign(net, createNetPatch(params, net)), - tls: Object.assign(tls, createTlsPatch(params, tls)) + http: mergeModules(http, createHttpPatch(params, http, resolveProxy)), + https: mergeModules(https, createHttpPatch(params, https, resolveProxy)), + net: mergeModules(net, createNetPatch(params, net)), + tls: mergeModules(tls, createTlsPatch(params, tls)) }; } diff --git a/src/vs/workbench/api/test/browser/extHost.api.impl.test.ts b/src/vs/workbench/api/test/browser/extHost.api.impl.test.ts index 35e7ff35360..d5db3ca4683 100644 --- a/src/vs/workbench/api/test/browser/extHost.api.impl.test.ts +++ b/src/vs/workbench/api/test/browser/extHost.api.impl.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { originalFSPath } from 'vs/base/common/resources'; import { isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts index b49d05ed00f..da5e8bb9e6f 100644 --- a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts +++ b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts @@ -17,7 +17,7 @@ import 'vs/editor/contrib/suggest/browser/suggest'; import 'vs/editor/contrib/rename/browser/rename'; import 'vs/editor/contrib/inlayHints/browser/inlayHintsController'; -import * as assert from 'assert'; +import assert from 'assert'; import { setUnexpectedErrorHandler, errorHandler } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; @@ -1275,6 +1275,22 @@ suite('ExtHostLanguageFeatureCommands', function () { }); + testApiCmd('DocumentLink[] vscode.executeLinkProvider returns lack tooltip #213970', async function () { + disposables.push(extHost.registerDocumentLinkProvider(nullExtensionDescription, defaultSelector, { + provideDocumentLinks(): any { + const link = new types.DocumentLink(new types.Range(0, 0, 0, 20), URI.parse('foo:bar')); + link.tooltip = 'Link Tooltip'; + return [link]; + } + })); + + await rpcProtocol.sync(); + + const links1 = await commands.executeCommand('vscode.executeLinkProvider', model.uri); + assert.strictEqual(links1.length, 1); + assert.strictEqual(links1[0].tooltip, 'Link Tooltip'); + }); + test('Color provider', function () { diff --git a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts index dd77886bbf0..de02ffff745 100644 --- a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts +++ b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; diff --git a/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts b/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts index 045f3d77cc1..be11d3a7353 100644 --- a/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts +++ b/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { MainContext, IWorkspaceEditDto, MainThreadBulkEditsShape, IWorkspaceTextEditDto } from 'vs/workbench/api/common/extHost.protocol'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/extHostCommands.test.ts b/src/vs/workbench/api/test/browser/extHostCommands.test.ts index 5697ffe78cf..5353ca9c069 100644 --- a/src/vs/workbench/api/test/browser/extHostCommands.test.ts +++ b/src/vs/workbench/api/test/browser/extHostCommands.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { MainThreadCommandsShape } from 'vs/workbench/api/common/extHost.protocol'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; diff --git a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts index ef43b937a98..298299b3e56 100644 --- a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; diff --git a/src/vs/workbench/api/test/browser/extHostDecorations.test.ts b/src/vs/workbench/api/test/browser/extHostDecorations.test.ts index 26b419f6e06..8dca84bc5c1 100644 --- a/src/vs/workbench/api/test/browser/extHostDecorations.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts index 4c30d01c4a7..98ae18a9caa 100644 --- a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DiagnosticCollection, ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; import { Diagnostic, DiagnosticSeverity, Range, DiagnosticRelatedInformation, Location } from 'vs/workbench/api/common/extHostTypes'; diff --git a/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts index 2132482c3aa..f4795adebb4 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; diff --git a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts index 3f60f5ef898..298a2811954 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import { Position } from 'vs/workbench/api/common/extHostTypes'; diff --git a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts index 632487d43c0..9f0a28ec02a 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; diff --git a/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts index 3f4255c49c8..eda97538749 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; diff --git a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts index cedfaa5e426..75f2ccce2d7 100644 --- a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts +++ b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type * as vscode from 'vscode'; -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; diff --git a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts index c35b3572cf9..a86c4b31b49 100644 --- a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts +++ b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ExtHostFileSystemEventService } from 'vs/workbench/api/common/extHostFileSystemEventService'; import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts index 585e3745c10..6b7de3ca716 100644 --- a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { setUnexpectedErrorHandler, errorHandler } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts b/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts index 465663c8288..a88e5a79bb9 100644 --- a/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts +++ b/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MainThreadMessageService } from 'vs/workbench/api/browser/mainThreadMessageService'; import { IDialogService, IPrompt, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice, IPromptOptions, IStatusMessageOptions, INotificationSource, INotificationSourceFilter, NotificationsFilter } from 'vs/platform/notification/common/notification'; diff --git a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts index 5a7ed7e4e70..49a2f011645 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as vscode from 'vscode'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; diff --git a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts index 5a7e6f434c2..a1341bad58c 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Barrier } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -344,4 +344,3 @@ suite('NotebookKernel', function () { assert.ok(found); }); }); - diff --git a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts index 97bfb308b9f..54878ddb0cd 100644 --- a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ExtensionIdentifier, IExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -64,7 +64,8 @@ suite('ExtHostTelemetry', function () { publisher: 'vscode', version: '1.0.0', engines: { vscode: '*' }, - extensionLocation: URI.parse('fake') + extensionLocation: URI.parse('fake'), + enabledApiProposals: undefined, }; const createExtHostTelemetry = () => { diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index b82376cd88d..c4515796d05 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -14,7 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { mock, mockObject, MockObject } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import * as editorRange from 'vs/editor/common/core/range'; -import { ExtensionIdentifier, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; import { MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -23,7 +23,7 @@ import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { ExtHostTesting, TestRunCoordinator, TestRunDto, TestRunProfileImpl } from 'vs/workbench/api/common/extHostTesting'; import { ExtHostTestItemCollection, TestItemImpl } from 'vs/workbench/api/common/extHostTestItem'; import * as convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { Location, Position, Range, TestMessage, TestResultState, TestRunProfileKind, TestRunRequest as TestRunRequestImpl, TestTag } from 'vs/workbench/api/common/extHostTypes'; +import { Location, Position, Range, TestMessage, TestRunProfileKind, TestRunRequest as TestRunRequestImpl, TestTag } from 'vs/workbench/api/common/extHostTypes'; import { AnyCallRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestDiffOpType, TestItemExpandState, TestMessageType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; @@ -637,7 +637,7 @@ suite('ExtHost Testing', () => { let req: TestRunRequest; let dto: TestRunDto; - const ext: IRelaxedExtensionDescription = {} as any; + const ext: IExtensionDescription = {} as any; teardown(() => { for (const { id } of c.trackers) { @@ -864,29 +864,6 @@ suite('ExtHost Testing', () => { assert.strictEqual(proxy.$appendTestMessagesInRun.called, false); }); - test('excludes tests outside tree or explicitly excluded', () => { - const task = c.createTestRun(ext, 'ctrlId', single, { - profile: configuration, - include: [single.root.children.get('id-a')!], - exclude: [single.root.children.get('id-a')!.children.get('id-aa')!], - preserveFocus: false, - }, 'hello world', false); - - task.passed(single.root.children.get('id-a')!.children.get('id-aa')!); - task.passed(single.root.children.get('id-a')!.children.get('id-ab')!); - - assert.deepStrictEqual(proxy.$updateTestStateInRun.args.length, 1); - const args = proxy.$updateTestStateInRun.args[0]; - assert.deepStrictEqual(proxy.$updateTestStateInRun.args, [[ - args[0], - args[1], - new TestId(['ctrlId', 'id-a', 'id-ab']).toString(), - TestResultState.Passed, - undefined, - ]]); - task.end(); - }); - test('sets state of test with identical local IDs (#131827)', () => { const testA = single.root.children.get('id-a'); const testB = single.root.children.get('id-b'); diff --git a/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts b/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts index 106a30df4f8..98bc7766151 100644 --- a/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Lazy } from 'vs/base/common/lazy'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; diff --git a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts index 3f780a03299..1e442c1ed88 100644 --- a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { Emitter } from 'vs/base/common/event'; import { ExtHostTreeViews } from 'vs/workbench/api/common/extHostTreeViews'; diff --git a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts index ab38b202b6e..40a148042ff 100644 --- a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { MarkdownString, NotebookCellOutputItem, NotebookData, LanguageSelector, WorkspaceEdit } from 'vs/workbench/api/common/extHostTypeConverters'; import { isEmptyObject } from 'vs/base/common/types'; diff --git a/src/vs/workbench/api/test/browser/extHostTypes.test.ts b/src/vs/workbench/api/test/browser/extHostTypes.test.ts index 6cef861c9c8..3d1ff69b232 100644 --- a/src/vs/workbench/api/test/browser/extHostTypes.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypes.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import * as types from 'vs/workbench/api/common/extHostTypes'; import { isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/api/test/browser/extHostWebview.test.ts b/src/vs/workbench/api/test/browser/extHostWebview.test.ts index b741292bd00..e87300aeb05 100644 --- a/src/vs/workbench/api/test/browser/extHostWebview.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWebview.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts index cc7b230c951..02d795e8e48 100644 --- a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { basename } from 'vs/base/common/path'; import { URI, UriComponents } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/mainThreadBulkEdits.test.ts b/src/vs/workbench/api/test/browser/mainThreadBulkEdits.test.ts index 959705f233c..c6ef6939bfe 100644 --- a/src/vs/workbench/api/test/browser/mainThreadBulkEdits.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadBulkEdits.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IWorkspaceTextEditDto } from 'vs/workbench/api/common/extHost.protocol'; import { mock } from 'vs/base/test/common/mock'; import { Event } from 'vs/base/common/event'; diff --git a/src/vs/workbench/api/test/browser/mainThreadCommands.test.ts b/src/vs/workbench/api/test/browser/mainThreadCommands.test.ts index 0c5d47544d1..d7503ca0d50 100644 --- a/src/vs/workbench/api/test/browser/mainThreadCommands.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadCommands.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MainThreadCommands } from 'vs/workbench/api/browser/mainThreadCommands'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; diff --git a/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts b/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts index a3a8e47755c..0c5a02f6606 100644 --- a/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { URI } from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts b/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts index f0960f7026e..c305000629f 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { URI, UriComponents } from 'vs/base/common/uri'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts index 8eaa58798d7..949c2a78ffa 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { MainThreadDocumentContentProviders } from 'vs/workbench/api/browser/mainThreadDocumentContentProviders'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; diff --git a/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts index 8331e51cc03..fee65f21119 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { BoundModelReferenceCollection } from 'vs/workbench/api/browser/mainThreadDocuments'; import { timeout } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts index d7190f1fe5c..f0d0538ce22 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MainThreadDocumentsAndEditors } from 'vs/workbench/api/browser/mainThreadDocumentsAndEditors'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; diff --git a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts index 9809814a6c0..1ca165c1b5f 100644 --- a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { DisposableStore, IReference, ImmortalReference } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts b/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts index 8a714d8130d..dd20e3cd70c 100644 --- a/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { disposableTimeout, timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts index e6460e3dcf2..aeaa4860e04 100644 --- a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as assert from 'assert'; +import assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; diff --git a/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts b/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts index 5234d7abb34..c784f418950 100644 --- a/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts index 3a56656608b..bfd874acf2e 100644 --- a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts +++ b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { promiseWithResolvers, timeout } from 'vs/base/common/async'; +import { Mutable } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; import { ActivatedExtension, EmptyExtension, ExtensionActivationTimes, ExtensionsActivator, IExtensionsActivatorHost } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ExtensionDescriptionRegistry, IActivationEventsReader } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; @@ -85,8 +86,8 @@ suite('ExtensionsActivator', () => { test('Supports having resolved extensions', async () => { const host = new SimpleExtensionsActivatorHost(); const bExt = desc(idB); - delete (bExt).main; - delete (bExt).browser; + delete (>bExt).main; + delete (>bExt).browser; const activator = createActivator(host, [ desc(idA, [idB]) ], [bExt]); @@ -103,7 +104,7 @@ suite('ExtensionsActivator', () => { [idB, extActivationB] ]); const bExt = desc(idB); - (bExt).api = 'none'; + (>bExt).api = 'none'; const activator = createActivator(host, [ desc(idA, [idB]) ], [bExt]); @@ -274,7 +275,8 @@ suite('ExtensionsActivator', () => { activationEvents, main: 'index.js', targetPlatform: TargetPlatform.UNDEFINED, - extensionDependencies: deps.map(d => d.value) + extensionDependencies: deps.map(d => d.value), + enabledApiProposals: undefined, }; } diff --git a/src/vs/workbench/api/test/common/extensionHostMain.test.ts b/src/vs/workbench/api/test/common/extensionHostMain.test.ts index 928c06a1b2c..1608511b527 100644 --- a/src/vs/workbench/api/test/common/extensionHostMain.test.ts +++ b/src/vs/workbench/api/test/common/extensionHostMain.test.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { SerializedError, errorHandler, onUnexpectedError } from 'vs/base/common/errors'; import { isFirefox, isSafari } from 'vs/base/common/platform'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; @@ -48,7 +48,7 @@ suite('ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slo declare readonly _serviceBrand: undefined; getExtensionPathIndex() { return new class extends ExtensionPaths { - override findSubstr(key: URI): Readonly | undefined { + override findSubstr(key: URI): IExtensionDescription | undefined { findSubstrCount++; return nullExtensionDescription; } diff --git a/src/vs/workbench/api/test/node/extHostSearch.test.ts b/src/vs/workbench/api/test/node/extHostSearch.test.ts index 1502ef3f565..af20a5e455b 100644 --- a/src/vs/workbench/api/test/node/extHostSearch.test.ts +++ b/src/vs/workbench/api/test/node/extHostSearch.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mapArrayOrNot } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -16,6 +16,7 @@ import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { MainContext, MainThreadSearchShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostConfigProvider, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration.js'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { Range } from 'vs/workbench/api/common/extHostTypes'; import { URITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; @@ -144,6 +145,26 @@ suite('ExtHostSearch', () => { rpcProtocol, new class extends mock() { override remote = { isRemote: false, authority: undefined, connectionData: null }; }, new URITransformerService(null), + new class extends mock() { + override async getConfigProvider(): Promise { + return { + onDidChangeConfiguration(_listener: (event: vscode.ConfigurationChangeEvent) => void) { }, + getConfiguration(): vscode.WorkspaceConfiguration { + return { + get() { }, + has() { + return false; + }, + inspect() { + return undefined; + }, + async update() { } + }; + }, + + } as ExtHostConfigProvider; + } + }, logService ); this._pfs = mockPFS as any; diff --git a/src/vs/workbench/api/test/node/extHostTunnelService.test.ts b/src/vs/workbench/api/test/node/extHostTunnelService.test.ts index 2f567ef7f4b..57fe15e52b4 100644 --- a/src/vs/workbench/api/test/node/extHostTunnelService.test.ts +++ b/src/vs/workbench/api/test/node/extHostTunnelService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { findPorts, getRootProcesses, getSockets, loadConnectionTable, loadListeningPorts, parseIpAddress, tryFindRootPorts } from 'vs/workbench/api/node/extHostTunnelService'; const tcp = diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 2c20a3572e2..7823fa988c0 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -63,7 +63,7 @@ class InspectContextKeysAction extends Action2 { const hoverFeedback = document.createElement('div'); const activeDocument = getActiveDocument(); activeDocument.body.appendChild(hoverFeedback); - disposables.add(toDisposable(() => activeDocument.body.removeChild(hoverFeedback))); + disposables.add(toDisposable(() => hoverFeedback.remove())); hoverFeedback.style.position = 'absolute'; hoverFeedback.style.pointerEvents = 'none'; diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index db6890b9017..188a94e1f51 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -725,7 +725,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const elementWithHover = getCustomHoverForElement(focusedElement as HTMLElement); if (elementWithHover) { - accessor.get(IHoverService).triggerUpdatableHover(elementWithHover as HTMLElement); + accessor.get(IHoverService).showManagedHover(elementWithHover as HTMLElement); } }, }); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 8137f8f0185..5b4fee44df6 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -14,7 +14,7 @@ import { SIDEBAR_PART_MINIMUM_WIDTH, SidebarPart } from 'vs/workbench/browser/pa import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; import { Position, Parts, PanelOpensMaximizedOptions, IWorkbenchLayoutService, positionFromString, positionToString, panelOpensMaximizedFromString, PanelAlignment, ActivityBarPosition, LayoutSettings, MULTI_WINDOW_PARTS, SINGLE_WINDOW_PARTS, ZenModeSettings, EditorTabsMode, EditorActionsLocation, shouldShowCustomTitleBar } from 'vs/workbench/services/layout/browser/layoutService'; import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITitleService } from 'vs/workbench/services/title/browser/titleService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -222,11 +222,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private getContainerDimension(container: HTMLElement): IDimension { if (container === this.mainContainer) { - // main window - return this.mainContainerDimension; + return this.mainContainerDimension; // main window } else { - // auxiliary window - return getClientArea(container); + return getClientArea(container); // auxiliary window } } @@ -680,7 +678,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.stateModel.setRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN, false); } - this.stateModel.onDidChangeState(change => { + this._register(this.stateModel.onDidChangeState(change => { // --- Start Positron --- if (change.key === LayoutStateKeys.POSITRON_TOP_ACTION_BAR_HIDDEN) { this.setPositronTopActionBarHidden(change.value as boolean); @@ -708,7 +706,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } this.doUpdateLayoutConfiguration(); - }); + })); // Layout Initialization State const initialEditorsState = this.getInitialEditorsState(); @@ -754,7 +752,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Only restore last viewlet if window was reloaded or we are in development mode let viewContainerToRestore: string | undefined; - if (!this.environmentService.isBuilt || lifecycleService.startupKind === StartupKind.ReloadedWindow) { + if ( + !this.environmentService.isBuilt || + lifecycleService.startupKind === StartupKind.ReloadedWindow || + this.environmentService.isExtensionDevelopment && !this.environmentService.extensionTestsLocationURI + ) { viewContainerToRestore = this.storageService.get(SidebarPart.activeViewletSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id); } else { viewContainerToRestore = this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id; @@ -1737,28 +1739,27 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi })); } - this._register(this.storageService.onWillSaveState(willSaveState => { - if (willSaveState.reason === WillSaveStateReason.SHUTDOWN) { - // Side Bar Size - const sideBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN) - ? this.workbenchGrid.getViewCachedVisibleSize(this.sideBarPartView) - : this.workbenchGrid.getViewSize(this.sideBarPartView).width; - this.stateModel.setInitializationValue(LayoutStateKeys.SIDEBAR_SIZE, sideBarSize as number); + this._register(this.storageService.onWillSaveState(e => { - // Panel Size - const panelSize = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) - ? this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) - : (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION) === Position.BOTTOM ? this.workbenchGrid.getViewSize(this.panelPartView).height : this.workbenchGrid.getViewSize(this.panelPartView).width); - this.stateModel.setInitializationValue(LayoutStateKeys.PANEL_SIZE, panelSize as number); + // Side Bar Size + const sideBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN) + ? this.workbenchGrid.getViewCachedVisibleSize(this.sideBarPartView) + : this.workbenchGrid.getViewSize(this.sideBarPartView).width; + this.stateModel.setInitializationValue(LayoutStateKeys.SIDEBAR_SIZE, sideBarSize as number); - // Auxiliary Bar Size - const auxiliaryBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN) - ? this.workbenchGrid.getViewCachedVisibleSize(this.auxiliaryBarPartView) - : this.workbenchGrid.getViewSize(this.auxiliaryBarPartView).width; - this.stateModel.setInitializationValue(LayoutStateKeys.AUXILIARYBAR_SIZE, auxiliaryBarSize as number); + // Panel Size + const panelSize = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) + ? this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) + : (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION) === Position.BOTTOM ? this.workbenchGrid.getViewSize(this.panelPartView).height : this.workbenchGrid.getViewSize(this.panelPartView).width); + this.stateModel.setInitializationValue(LayoutStateKeys.PANEL_SIZE, panelSize as number); - this.stateModel.save(true, true); - } + // Auxiliary Bar Size + const auxiliaryBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN) + ? this.workbenchGrid.getViewCachedVisibleSize(this.auxiliaryBarPartView) + : this.workbenchGrid.getViewSize(this.auxiliaryBarPartView).width; + this.stateModel.setInitializationValue(LayoutStateKeys.AUXILIARYBAR_SIZE, auxiliaryBarSize as number); + + this.stateModel.save(true, true); })); } @@ -2153,6 +2154,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi height: panelPosition === Position.BOTTOM ? this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT) : size.height }); } + this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_WAS_LAST_MAXIMIZED, !isMaximized); } @@ -2712,7 +2714,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi visible: !this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) }; - const middleSection: ISerializedNode[] = this.arrangeMiddleSectionNodes({ activityBar: activityBarNode, auxiliaryBar: auxiliaryBarNode, @@ -3116,6 +3117,7 @@ class LayoutStateModel extends Disposable { if (oldValue !== undefined) { return !oldValue; } + return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.DEFAULT; } diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 35f856b931a..6c9dbb9a0c9 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -87,6 +87,15 @@ body.web { text-decoration: none; } + +.monaco-workbench p > a { + text-decoration: var(--text-link-decoration); +} + +.monaco-workbench.underline-links { + --text-link-decoration: underline; +} + .monaco-workbench.hc-black p > a, .monaco-workbench.hc-light p > a { text-decoration: underline !important; diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index f42d3307fb7..b40341d217c 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -104,7 +104,8 @@ display: none; } -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.clicked:focus:before { +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.clicked:focus:before, +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.clicked:focus .active-item-indicator::before { border-left: none !important; /* no focus feedback when using mouse */ } diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index c86d7fd78f5..cbc4783066e 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -422,7 +422,7 @@ export abstract class CompositePart extends Part { const titleContainer = append(parent, $('.title-label')); const titleLabel = append(titleContainer, $('h2')); this.titleLabelElement = titleLabel; - const hover = this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), titleLabel, '')); + const hover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), titleLabel, '')); const $this = this; return { diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 67d4b7173d4..63e2103bcee 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -37,6 +37,7 @@ import { ActiveEditorAvailableEditorIdsContext, ActiveEditorContext, ActiveEdito import { URI } from 'vs/base/common/uri'; import { getActiveDocument } from 'vs/base/browser/dom'; import { ICommandActionTitle } from 'vs/platform/action/common/action'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; class ExecuteCommandAction extends Action2 { @@ -564,6 +565,8 @@ abstract class AbstractCloseAllAction extends Action2 { override async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); + const logService = accessor.get(ILogService); + const progressService = accessor.get(IProgressService); const editorGroupService = accessor.get(IEditorGroupsService); const filesConfigurationService = accessor.get(IFilesConfigurationService); const fileDialogService = accessor.get(IFileDialogService); @@ -636,7 +639,7 @@ abstract class AbstractCloseAllAction extends Action2 { case ConfirmResult.CANCEL: return; case ConfirmResult.DONT_SAVE: - await editorService.revert(editors, { soft: true }); + await this.revertEditors(editorService, logService, progressService, editors); break; case ConfirmResult.SAVE: await editorService.save(editors, { reason: SaveReason.EXPLICIT }); @@ -656,7 +659,7 @@ abstract class AbstractCloseAllAction extends Action2 { case ConfirmResult.CANCEL: return; case ConfirmResult.DONT_SAVE: - await editorService.revert(editors, { soft: true }); + await this.revertEditors(editorService, logService, progressService, editors); break; case ConfirmResult.SAVE: await editorService.save(editors, { reason: SaveReason.EXPLICIT }); @@ -686,6 +689,33 @@ abstract class AbstractCloseAllAction extends Action2 { return this.doCloseAll(editorGroupService); } + private revertEditors(editorService: IEditorService, logService: ILogService, progressService: IProgressService, editors: IEditorIdentifier[]): Promise { + return progressService.withProgress({ + location: ProgressLocation.Window, // use window progress to not be too annoying about this operation + delay: 800, // delay so that it only appears when operation takes a long time + title: localize('reverting', "Reverting Editors..."), + }, () => this.doRevertEditors(editorService, logService, editors)); + } + + private async doRevertEditors(editorService: IEditorService, logService: ILogService, editors: IEditorIdentifier[]): Promise { + try { + // We first attempt to revert all editors with `soft: false`, to ensure that + // working copies revert to their state on disk. Even though we close editors, + // it is possible that other parties hold a reference to the working copy + // and expect it to be in a certain state after the editor is closed without + // saving. + await editorService.revert(editors); + } catch (error) { + logService.error(error); + + // if that fails, since we are about to close the editor, we accept that + // the editor cannot be reverted and instead do a soft revert that just + // enables us to close the editor. With this, a user can always close a + // dirty editor even when reverting fails. + await editorService.revert(editors, { soft: true }); + } + } + private async revealEditorsToConfirm(editors: ReadonlyArray, editorGroupService: IEditorGroupsService): Promise { try { const handledGroups = new Set(); diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index c8670de6fb0..a8ba531496e 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -31,7 +31,7 @@ import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/br import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; import { ActiveEditorCanSplitInGroupContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupLockedContext, ActiveEditorStickyContext, MultipleEditorGroupsContext, SideBySideEditorActiveContext, TextCompareEditorActiveContext } from 'vs/workbench/common/contextkeys'; -import { CloseDirection, EditorInputCapabilities, EditorsOrder, IEditorCommandsContext, IEditorIdentifier, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, IVisibleEditorPane, isEditorIdentifier, isEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; +import { CloseDirection, EditorInputCapabilities, EditorsOrder, IEditorCommandsContext, IEditorIdentifier, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, IVisibleEditorPane, isEditorCommandsContext, isEditorIdentifier, isEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; @@ -1404,7 +1404,7 @@ export function getCommandsContext(accessor: ServicesAccessor, resourceOrContext const isUri = URI.isUri(resourceOrContext); const editorCommandsContext = isUri ? context : resourceOrContext ? resourceOrContext : context; - if (editorCommandsContext) { + if (editorCommandsContext && isEditorCommandsContext(editorCommandsContext)) { return editorCommandsContext; } diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 463b527e3b6..b11c66e6057 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -92,7 +92,7 @@ class DropOverlay extends Themable { this.groupView.element.appendChild(container); this.groupView.element.classList.add('dragged-over'); this._register(toDisposable(() => { - this.groupView.element.removeChild(container); + container.remove(); this.groupView.element.classList.remove('dragged-over'); })); diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index 5d638871273..ef1b25dd681 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -466,7 +466,7 @@ export class EditorPanes extends Disposable { // Remove editor pane from parent const editorPaneContainer = this._activeEditorPane.getContainer(); if (editorPaneContainer) { - this.editorPanesParent.removeChild(editorPaneContainer); + editorPaneContainer.remove(); hide(editorPaneContainer); } diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index e505a3fd466..18123131e5b 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -761,7 +761,7 @@ export class EditorParts extends MultiWindowParts implements IEditor let groupRegisteredContextKeys = this.registeredContextKeys.get(group.id); if (!groupRegisteredContextKeys) { groupRegisteredContextKeys = new Map(); - this.scopedContextKeys.set(group.id, groupRegisteredContextKeys); + this.registeredContextKeys.set(group.id, groupRegisteredContextKeys); } let scopedRegisteredContextKey = groupRegisteredContextKeys.get(provider.contextKey.key); diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index c2ceeb2d7d7..845160784b1 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -1079,11 +1079,20 @@ export class ChangeLanguageAction extends Action2 { weight: KeybindingWeight.WorkbenchContrib, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyM) }, - precondition: ContextKeyExpr.not('notebookEditorFocused') + precondition: ContextKeyExpr.not('notebookEditorFocused'), + metadata: { + description: localize('changeLanguageMode.description', "Change the language mode of the active text editor."), + args: [ + { + name: localize('changeLanguageMode.arg.name', "The name of the language mode to change to."), + constraint: (value: any) => typeof value === 'string', + } + ] + } }); } - override async run(accessor: ServicesAccessor): Promise { + override async run(accessor: ServicesAccessor, languageMode?: string): Promise { const quickInputService = accessor.get(IQuickInputService); const editorService = accessor.get(IEditorService); const languageService = accessor.get(ILanguageService); @@ -1162,7 +1171,7 @@ export class ChangeLanguageAction extends Action2 { }; picks.unshift(autoDetectLanguage); - const pick = await quickInputService.pick(picks, { placeHolder: localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }); + const pick = typeof languageMode === 'string' ? { label: languageMode } : await quickInputService.pick(picks, { placeHolder: localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }); if (!pick) { return; } @@ -1444,13 +1453,16 @@ export class ChangeEncodingAction extends Action2 { let guessedEncoding: string | undefined = undefined; if (fileService.hasProvider(resource)) { - const content = await textFileService.readStream(resource, { autoGuessEncoding: true }); + const content = await textFileService.readStream(resource, { + autoGuessEncoding: true, + candidateGuessEncodings: textResourceConfigurationService.getValue(resource, 'files.candidateGuessEncodings') + }); guessedEncoding = content.encoding; } const isReopenWithEncoding = (action === reopenWithEncodingPick); - const configuredEncoding = textResourceConfigurationService.getValue(resource ?? undefined, 'files.encoding'); + const configuredEncoding = textResourceConfigurationService.getValue(resource, 'files.encoding'); let directMatchIndex: number | undefined; let aliasMatchIndex: number | undefined; diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index c3d7a0cce9d..2dab828b50a 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -160,7 +160,7 @@ export class SideBySideEditor extends AbstractEditorWithViewState { + if (e.event.removed) { + for (const removed of e.event.removed) { + this.removeAccount(e.providerId, removed.account); + } + } for (const changed of [...(e.event.changed ?? []), ...(e.event.added ?? [])]) { try { await this.addOrUpdateAccount(e.providerId, changed.account); @@ -344,11 +349,6 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction this.logService.error(e); } } - if (e.event.removed) { - for (const removed of e.event.removed) { - this.removeAccount(e.providerId, removed.account); - } - } })); } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 5d29de1726a..c42e49f2629 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -602,7 +602,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast if (visible) { notificationsToastsContainer.appendChild(toast.container); } else { - notificationsToastsContainer.removeChild(toast.container); + toast.container.remove(); } // Update visibility in model diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 65ad1afe6cc..c1fda244291 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -30,7 +30,7 @@ import { defaultButtonStyles, defaultProgressBarStyles } from 'vs/platform/theme import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; export class NotificationsListDelegate implements IListVirtualDelegate { @@ -379,14 +379,14 @@ export class NotificationTemplateRenderer extends Disposable { this.renderSeverity(notification); // Message - const messageCustomHover = this.inputDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.template.message, '')); + const messageCustomHover = this.inputDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.template.message, '')); const messageOverflows = this.renderMessage(notification, messageCustomHover); // Secondary Actions this.renderSecondaryActions(notification, messageOverflows); // Source - const sourceCustomHover = this.inputDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.template.source, '')); + const sourceCustomHover = this.inputDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.template.source, '')); this.renderSource(notification, sourceCustomHover); // Buttons @@ -424,7 +424,7 @@ export class NotificationTemplateRenderer extends Disposable { this.template.icon.classList.add(...ThemeIcon.asClassNameArray(this.toSeverityIcon(notification.severity))); } - private renderMessage(notification: INotificationViewItem, customHover: IUpdatableHover): boolean { + private renderMessage(notification: INotificationViewItem, customHover: IManagedHover): boolean { clearNode(this.template.message); this.template.message.appendChild(NotificationMessageRenderer.render(notification.message, { callback: link => this.openerService.open(URI.parse(link), { allowCommands: true }), @@ -474,7 +474,7 @@ export class NotificationTemplateRenderer extends Disposable { actions.forEach(action => this.template.toolbar.push(action, { icon: true, label: false, keybinding: this.getKeybindingLabel(action) })); } - private renderSource(notification: INotificationViewItem, sourceCustomHover: IUpdatableHover): void { + private renderSource(notification: INotificationViewItem, sourceCustomHover: IManagedHover): void { if (notification.expanded && notification.source) { this.template.source.textContent = localize('notificationSource', "Source: {0}", notification.source); sourceCustomHover.update(notification.source); diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index d6897bf02d7..b30d2bee088 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -88,6 +88,11 @@ margin-left: 3px; } +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-left.compact-right > .statusbar-item-label { + margin-right:0; + margin-left: 0; +} + .monaco-workbench .part.statusbar > .items-container > .statusbar-item.left.first-visible-item { padding-left: 7px; /* Add padding to the most left status bar item */ } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts index 07dd5640c0c..d458ce91e79 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts @@ -24,7 +24,7 @@ import { spinningLoading, syncing } from 'vs/platform/theme/common/iconRegistry' import { isMarkdownString, markdownStringEqual } from 'vs/base/common/htmlContent'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; export class StatusbarEntryItem extends Disposable { @@ -42,7 +42,7 @@ export class StatusbarEntryItem extends Disposable { private readonly focusListener = this._register(new MutableDisposable()); private readonly focusOutListener = this._register(new MutableDisposable()); - private hover: IUpdatableHover | undefined = undefined; + private hover: IManagedHover | undefined = undefined; readonly labelContainer: HTMLElement; readonly beakContainer: HTMLElement; @@ -122,7 +122,7 @@ export class StatusbarEntryItem extends Disposable { if (this.hover) { this.hover.update(hoverContents); } else { - this.hover = this._register(this.hoverService.setupUpdatableHover(this.hoverDelegate, this.container, hoverContents)); + this.hover = this._register(this.hoverService.setupManagedHover(this.hoverDelegate, this.container, hoverContents)); } if (entry.command !== ShowTooltipCommand /* prevents flicker on click */) { this.focusListener.value = addDisposableListener(this.labelContainer, EventType.FOCUS, e => { @@ -283,7 +283,7 @@ class StatusBarCodiconLabel extends SimpleIconLabel { private progressCodicon = renderIcon(syncing); private currentText = ''; - private currentShowProgress: boolean | 'syncing' | 'loading' = false; + private currentShowProgress: boolean | 'loading' | 'syncing' = false; constructor( private readonly container: HTMLElement @@ -291,10 +291,10 @@ class StatusBarCodiconLabel extends SimpleIconLabel { super(container); } - set showProgress(showProgress: boolean | 'syncing' | 'loading') { + set showProgress(showProgress: boolean | 'loading' | 'syncing') { if (this.currentShowProgress !== showProgress) { this.currentShowProgress = showProgress; - this.progressCodicon = renderIcon(showProgress === 'loading' ? spinningLoading : syncing); + this.progressCodicon = renderIcon(showProgress === 'syncing' ? syncing : spinningLoading); this.text = this.currentText; } } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts index 8fd7e353217..8c0ecf356a2 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts @@ -258,19 +258,34 @@ export class StatusbarViewModel extends Disposable { // - those with `priority: number` that can be compared // - those with `priority: string` that must be sorted // relative to another entry if possible - const mapEntryWithNumberedPriorityToIndex = new Map(); - const mapEntryWithRelativePriority = new Map(); + const mapEntryWithNumberedPriorityToIndex = new Map(); + const mapEntryWithRelativePriority = new Map>(); for (let i = 0; i < this._entries.length; i++) { const entry = this._entries[i]; if (typeof entry.priority.primary === 'number') { mapEntryWithNumberedPriorityToIndex.set(entry, i); } else { - let entries = mapEntryWithRelativePriority.get(entry.priority.primary.id); + const referenceEntryId = entry.priority.primary.id; + let entries = mapEntryWithRelativePriority.get(referenceEntryId); if (!entries) { - entries = []; - mapEntryWithRelativePriority.set(entry.priority.primary.id, entries); + + // It is possible that this entry references another entry + // that itself references an entry. In that case, we want + // to add it to the entries of the referenced entry. + + for (const relativeEntries of mapEntryWithRelativePriority.values()) { + if (relativeEntries.has(referenceEntryId)) { + entries = relativeEntries; + break; + } + } + + if (!entries) { + entries = new Map(); + mapEntryWithRelativePriority.set(referenceEntryId, entries); + } } - entries.push(entry); + entries.set(entry.id, entry); } } @@ -311,7 +326,8 @@ export class StatusbarViewModel extends Disposable { sortedEntries = []; for (const entry of sortedEntriesWithNumberedPriority) { - const relativeEntries = mapEntryWithRelativePriority.get(entry.id); + const relativeEntriesMap = mapEntryWithRelativePriority.get(entry.id); + const relativeEntries = relativeEntriesMap ? Array.from(relativeEntriesMap.values()) : undefined; // Fill relative entries to LEFT if (relativeEntries) { @@ -333,7 +349,7 @@ export class StatusbarViewModel extends Disposable { // Finally, just append all entries that reference another entry // that does not exist to the end of the list for (const [, entries] of mapEntryWithRelativePriority) { - sortedEntries.push(...entries); + sortedEntries.push(...entries.values()); } } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index f938ea7b353..bac67111eae 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -433,7 +433,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { } // Figure out groups of entries with `compact` alignment - const compactEntryGroups = new Map>(); + const compactEntryGroups = new Map>(); for (const entry of mapIdToVisibleEntry.values()) { if ( isStatusbarEntryLocation(entry.priority.primary) && // entry references another entry as location @@ -448,11 +448,25 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { // Build a map of entries that are compact among each other let compactEntryGroup = compactEntryGroups.get(locationId); if (!compactEntryGroup) { - compactEntryGroup = new Set([entry, location]); - compactEntryGroups.set(locationId, compactEntryGroup); - } else { - compactEntryGroup.add(entry); + + // It is possible that this entry references another entry + // that itself references an entry. In that case, we want + // to add it to the entries of the referenced entry. + + for (const group of compactEntryGroups.values()) { + if (group.has(locationId)) { + compactEntryGroup = group; + break; + } + } + + if (!compactEntryGroup) { + compactEntryGroup = new Map(); + compactEntryGroups.set(locationId, compactEntryGroup); + } } + compactEntryGroup.set(entry.id, entry); + compactEntryGroup.set(location.id, location); // Adjust CSS classes to move compact items closer together if (entry.priority.primary.alignment === StatusbarAlignment.LEFT) { @@ -465,7 +479,6 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { } } - // Install mouse listeners to update hover feedback for // all compact entries that belong to each other const statusBarItemHoverBackground = this.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); @@ -473,7 +486,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { this.compactEntriesDisposable.value = new DisposableStore(); if (statusBarItemHoverBackground && statusBarItemCompactHoverBackground && !isHighContrast(this.theme.type)) { for (const [, compactEntryGroup] of compactEntryGroups) { - for (const compactEntry of compactEntryGroup) { + for (const compactEntry of compactEntryGroup.values()) { if (!compactEntry.hasCommand) { continue; // only show hover feedback when we have a command } diff --git a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts index 59b379f497d..88d5435936b 100644 --- a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts @@ -96,7 +96,7 @@ class CommandCenterCenterViewItem extends BaseActionViewItem { container.classList.add('command-center-center'); container.classList.toggle('multiple', (this._submenu.actions.length > 1)); - const hover = this._store.add(this._hoverService.setupUpdatableHover(this._hoverDelegate, container, this.getTooltip())); + const hover = this._store.add(this._hoverService.setupManagedHover(this._hoverDelegate, container, this.getTooltip())); // update label & tooltip when window title changes this._store.add(this._windowTitle.onDidChange(() => { @@ -157,7 +157,7 @@ class CommandCenterCenterViewItem extends BaseActionViewItem { labelElement.innerText = label; reset(container, searchIcon, labelElement); - const hover = this._store.add(that._hoverService.setupUpdatableHover(that._hoverDelegate, container, this.getTooltip())); + const hover = this._store.add(that._hoverService.setupManagedHover(that._hoverDelegate, container, this.getTooltip())); // update label & tooltip when window title changes this._store.add(that._windowTitle.onDidChange(() => { diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 62bedae0e23..9e83dbcc2e4 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -634,7 +634,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { this.editorToolbarMenuDisposables.add(this.actionToolBar.actionRunner); } else { this.actionToolBar.actionRunner = new ActionRunner(); - this.actionToolBar.context = {}; + this.actionToolBar.context = undefined; this.editorToolbarMenuDisposables.add(this.actionToolBar.actionRunner); } diff --git a/src/vs/workbench/browser/parts/views/checkbox.ts b/src/vs/workbench/browser/parts/views/checkbox.ts index 6d6125e4f5c..f428de9bffe 100644 --- a/src/vs/workbench/browser/parts/views/checkbox.ts +++ b/src/vs/workbench/browser/parts/views/checkbox.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Codicon } from 'vs/base/common/codicons'; @@ -28,7 +28,7 @@ export class TreeItemCheckbox extends Disposable { public toggle: Toggle | undefined; private checkboxContainer: HTMLDivElement; public isDisposed = false; - private hover: IUpdatableHover | undefined; + private hover: IManagedHover | undefined; public static readonly checkboxClass = 'custom-view-tree-node-item-checkbox'; @@ -87,7 +87,7 @@ export class TreeItemCheckbox extends Disposable { private setHover(checkbox: ITreeItemCheckboxState) { if (this.toggle) { if (!this.hover) { - this.hover = this._register(this.hoverService.setupUpdatableHover(this.hoverDelegate, this.toggle.domNode, this.checkboxHoverContent(checkbox))); + this.hover = this._register(this.hoverService.setupManagedHover(this.hoverDelegate, this.toggle.domNode, this.checkboxHoverContent(checkbox))); } else { this.hover.update(checkbox.tooltip); } @@ -122,7 +122,7 @@ export class TreeItemCheckbox extends Disposable { private removeCheckbox() { const children = this.checkboxContainer.children; for (const child of children) { - this.checkboxContainer.removeChild(child); + child.remove(); } } } diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index db78e940c5d..7c1b3619944 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -36,7 +36,7 @@ import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/plat import { Action2, IMenuService, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { FileKind } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -70,11 +70,12 @@ import { TelemetryTrustedValue } from 'vs/platform/telemetry/common/telemetryUti import { ITreeViewsDnDService } from 'vs/editor/common/services/treeViewsDndService'; import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd'; import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; -import type { IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { parseLinkedText } from 'vs/base/common/linkedText'; import { Button } from 'vs/base/browser/ui/button/button'; import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IAccessibleViewInformationService } from 'vs/workbench/services/accessibility/common/accessibleViewInformationService'; +import { Command } from 'vs/editor/common/languages'; export class TreeViewPane extends ViewPane { @@ -175,15 +176,22 @@ class Root implements ITreeItem { children: ITreeItem[] | undefined = undefined; } -function isTreeCommandEnabled(treeCommand: TreeCommand, contextKeyService: IContextKeyService): boolean { - const command = CommandsRegistry.getCommand(treeCommand.originalId ? treeCommand.originalId : treeCommand.id); +function commandPreconditions(commandId: string): ContextKeyExpression | undefined { + const command = CommandsRegistry.getCommand(commandId); if (command) { const commandAction = MenuRegistry.getCommand(command.id); - const precondition = commandAction && commandAction.precondition; - if (precondition) { - return contextKeyService.contextMatchesRules(precondition); - } + return commandAction && commandAction.precondition; } + return undefined; +} + +function isTreeCommandEnabled(treeCommand: TreeCommand | Command, contextKeyService: IContextKeyService): boolean { + const commandId: string = (treeCommand as TreeCommand).originalId ? (treeCommand as TreeCommand).originalId! : treeCommand.id; + const precondition = commandPreconditions(commandId); + if (precondition) { + return contextKeyService.contextMatchesRules(precondition); + } + return true; } @@ -863,6 +871,20 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { button.onDidClick(_ => { this.openerService.open(node.href, { allowCommands: true }); }, null, disposables); + + const href = URI.parse(node.href); + if (href.scheme === Schemas.command) { + const preConditions = commandPreconditions(href.path); + if (preConditions) { + button.enabled = this.contextKeyService.contextMatchesRules(preConditions); + disposables.add(this.contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(new Set(preConditions.keys()))) { + button.enabled = this.contextKeyService.contextMatchesRules(preConditions); + } + })); + } + } + disposables.add(button); hasFoundButton = true; result.push(buttonContainer); @@ -1202,7 +1224,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer { - this.paneElement.removeChild(this.container); + this.container.remove(); this.paneElement.classList.remove('dragged-over'); })); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 8494c963f3f..ae4d22c9eaa 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -102,9 +102,10 @@ const registry = Registry.as(ConfigurationExtensions.Con let customEditorLabelDescription = localize('workbench.editor.label.patterns', "Controls the rendering of the editor label. Each __Item__ is a pattern that matches a file path. Both relative and absolute file paths are supported. The relative path must include the WORKSPACE_FOLDER (e.g `WORKSPACE_FOLDER/src/**.tsx` or `*/src/**.tsx`). Absolute patterns must start with a `/`. In case multiple patterns match, the longest matching path will be picked. Each __Value__ is the template for the rendered editor when the __Item__ matches. Variables are substituted based on the context:"); customEditorLabelDescription += '\n- ' + [ localize('workbench.editor.label.dirname', "`${dirname}`: name of the folder in which the file is located (e.g. `WORKSPACE_FOLDER/folder/file.txt -> folder`)."), - localize('workbench.editor.label.nthdirname', "`${dirname(N)}`: name of the nth parent folder in which the file is located (e.g. `N=2: WORKSPACE_FOLDER/static/folder/file.txt -> WORKSPACE_FOLDER`). Folders can be picked from the start of the path by using negative numbers (e.g. `N=-1: WORKSPACE_FOLDER/folder/file.txt -> WORKSPACE_FOLDER`). If the __Item__ is an absolute pattern path, the first folder (`N=-1`) refers to the first folder in the absoulte path, otherwise it corresponds to the workspace folder."), + localize('workbench.editor.label.nthdirname', "`${dirname(N)}`: name of the nth parent folder in which the file is located (e.g. `N=2: WORKSPACE_FOLDER/static/folder/file.txt -> WORKSPACE_FOLDER`). Folders can be picked from the start of the path by using negative numbers (e.g. `N=-1: WORKSPACE_FOLDER/folder/file.txt -> WORKSPACE_FOLDER`). If the __Item__ is an absolute pattern path, the first folder (`N=-1`) refers to the first folder in the absolute path, otherwise it corresponds to the workspace folder."), localize('workbench.editor.label.filename', "`${filename}`: name of the file without the file extension (e.g. `WORKSPACE_FOLDER/folder/file.txt -> file`)."), localize('workbench.editor.label.extname', "`${extname}`: the file extension (e.g. `WORKSPACE_FOLDER/folder/file.txt -> txt`)."), + localize('workbench.editor.label.nthextname', "`${extname(N)}`: the nth extension of the file separated by '.' (e.g. `N=2: WORKSPACE_FOLDER/folder/file.ext1.ext2.ext3 -> ext1`). Extension can be picked from the start of the extension by using negative numbers (e.g. `N=-1: WORKSPACE_FOLDER/folder/file.ext1.ext2.ext3 -> ext2`)."), ].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations customEditorLabelDescription += '\n\n' + localize('customEditorLabelDescriptionExample', "Example: `\"**/static/**/*.html\": \"${filename} - ${dirname} (${extname})\"` will render a file `WORKSPACE_FOLDER/static/folder/file.html` as `file - folder (html)`."); @@ -112,8 +113,8 @@ const registry = Registry.as(ConfigurationExtensions.Con })(), additionalProperties: { - type: 'string', - markdownDescription: localize('workbench.editor.label.template', "The template which should be rendered when the pattern mtches. May include the variables ${dirname}, ${filename} and ${extname}."), + type: ['string', 'null'], + markdownDescription: localize('workbench.editor.label.template', "The template which should be rendered when the pattern matches. May include the variables ${dirname}, ${filename} and ${extname}."), minLength: 1, pattern: '.*[a-zA-Z0-9].*' }, diff --git a/src/vs/workbench/common/configuration.ts b/src/vs/workbench/common/configuration.ts index 01b4d2b510d..f4aff57ea85 100644 --- a/src/vs/workbench/common/configuration.ts +++ b/src/vs/workbench/common/configuration.ts @@ -234,7 +234,7 @@ export class DynamicWorkbenchSecurityConfiguration extends Disposable implements } } -const CONFIG_NEW_WINDOW_PROFILE = 'window.newWindowProfile'; +export const CONFIG_NEW_WINDOW_PROFILE = 'window.newWindowProfile'; export class DynamicWindowConfiguration extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/common/contributions.ts b/src/vs/workbench/common/contributions.ts index aaf1452c25a..f7e23fae0a2 100644 --- a/src/vs/workbench/common/contributions.ts +++ b/src/vs/workbench/common/contributions.ts @@ -11,7 +11,7 @@ import { mark } from 'vs/base/common/performance'; import { ILogService } from 'vs/platform/log/common/log'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { getOrSet } from 'vs/base/common/map'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, isDisposable } from 'vs/base/common/lifecycle'; import { IEditorPaneService } from 'vs/workbench/services/editor/common/editorPaneService'; /** @@ -156,6 +156,7 @@ export class WorkbenchContributionsRegistry extends Disposable implements IWorkb private readonly contributionsById = new Map(); private readonly instancesById = new Map(); + private readonly instanceDisposables = this._register(new DisposableStore()); private readonly timingsByPhase = new Map>(); get timings() { return this.timingsByPhase; } @@ -249,6 +250,11 @@ export class WorkbenchContributionsRegistry extends Disposable implements IWorkb const environmentService = this.environmentService = accessor.get(IEnvironmentService); const editorPaneService = this.editorPaneService = accessor.get(IEditorPaneService); + // Dispose contributions on shutdown + this._register(lifecycleService.onDidShutdown(() => { + this.instanceDisposables.clear(); + })); + // Instantiate contributions by phase when they are ready for (const phase of [LifecyclePhase.Starting, LifecyclePhase.Ready, LifecyclePhase.Restored, LifecyclePhase.Eventually]) { this.instantiateByPhase(instantiationService, lifecycleService, logService, environmentService, phase); @@ -377,6 +383,9 @@ export class WorkbenchContributionsRegistry extends Disposable implements IWorkb this.instancesById.set(contribution.id, instance); this.contributionsById.delete(contribution.id); } + if (isDisposable(instance)) { + this.instanceDisposables.add(instance); + } } catch (error) { logService.error(`Unable to create workbench contribution '${contribution.id ?? contribution.ctor.name}'.`, error); } finally { diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 17f33b5bff3..317efcfa7a8 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -51,19 +51,9 @@ export function WORKBENCH_BACKGROUND(theme: IColorTheme): Color { //#region Tab Background -export const TAB_ACTIVE_BACKGROUND = registerColor('tab.activeBackground', { - dark: editorBackground, - light: editorBackground, - hcDark: editorBackground, - hcLight: editorBackground -}, localize('tabActiveBackground', "Active tab background color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_ACTIVE_BACKGROUND = registerColor('tab.activeBackground', editorBackground, localize('tabActiveBackground', "Active tab background color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_UNFOCUSED_ACTIVE_BACKGROUND = registerColor('tab.unfocusedActiveBackground', { - dark: TAB_ACTIVE_BACKGROUND, - light: TAB_ACTIVE_BACKGROUND, - hcDark: TAB_ACTIVE_BACKGROUND, - hcLight: TAB_ACTIVE_BACKGROUND, -}, localize('tabUnfocusedActiveBackground', "Active tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_UNFOCUSED_ACTIVE_BACKGROUND = registerColor('tab.unfocusedActiveBackground', TAB_ACTIVE_BACKGROUND, localize('tabUnfocusedActiveBackground', "Active tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_INACTIVE_BACKGROUND = registerColor('tab.inactiveBackground', { dark: '#2D2D2D', @@ -72,12 +62,7 @@ export const TAB_INACTIVE_BACKGROUND = registerColor('tab.inactiveBackground', { hcLight: null, }, localize('tabInactiveBackground', "Inactive tab background color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_UNFOCUSED_INACTIVE_BACKGROUND = registerColor('tab.unfocusedInactiveBackground', { - dark: TAB_INACTIVE_BACKGROUND, - light: TAB_INACTIVE_BACKGROUND, - hcDark: TAB_INACTIVE_BACKGROUND, - hcLight: TAB_INACTIVE_BACKGROUND -}, localize('tabUnfocusedInactiveBackground', "Inactive tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_UNFOCUSED_INACTIVE_BACKGROUND = registerColor('tab.unfocusedInactiveBackground', TAB_INACTIVE_BACKGROUND, localize('tabUnfocusedInactiveBackground', "Inactive tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); //#endregion @@ -115,12 +100,7 @@ export const TAB_UNFOCUSED_INACTIVE_FOREGROUND = registerColor('tab.unfocusedIna //#region Tab Hover Foreground/Background -export const TAB_HOVER_BACKGROUND = registerColor('tab.hoverBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('tabHoverBackground', "Tab background color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_HOVER_BACKGROUND = registerColor('tab.hoverBackground', null, localize('tabHoverBackground', "Tab background color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_HOVER_BACKGROUND = registerColor('tab.unfocusedHoverBackground', { dark: transparent(TAB_HOVER_BACKGROUND, 0.5), @@ -129,12 +109,7 @@ export const TAB_UNFOCUSED_HOVER_BACKGROUND = registerColor('tab.unfocusedHoverB hcLight: null }, localize('tabUnfocusedHoverBackground', "Tab background color in an unfocused group when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_HOVER_FOREGROUND = registerColor('tab.hoverForeground', { - dark: null, - light: null, - hcDark: null, - hcLight: null, -}, localize('tabHoverForeground', "Tab foreground color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_HOVER_FOREGROUND = registerColor('tab.hoverForeground', null, localize('tabHoverForeground', "Tab foreground color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_HOVER_FOREGROUND = registerColor('tab.unfocusedHoverForeground', { dark: transparent(TAB_HOVER_FOREGROUND, 0.5), @@ -161,12 +136,7 @@ export const TAB_LAST_PINNED_BORDER = registerColor('tab.lastPinnedBorder', { hcLight: contrastBorder }, localize('lastPinnedTabBorder', "Border to separate pinned tabs from other tabs. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_ACTIVE_BORDER = registerColor('tab.activeBorder', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('tabActiveBorder', "Border on the bottom of an active tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_ACTIVE_BORDER = registerColor('tab.activeBorder', null, localize('tabActiveBorder', "Border on the bottom of an active tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_ACTIVE_BORDER = registerColor('tab.unfocusedActiveBorder', { dark: transparent(TAB_ACTIVE_BORDER, 0.5), @@ -189,34 +159,14 @@ export const TAB_UNFOCUSED_ACTIVE_BORDER_TOP = registerColor('tab.unfocusedActiv hcLight: '#B5200D' }, localize('tabActiveUnfocusedBorderTop', "Border to the top of an active tab in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_SELECTED_BORDER_TOP = registerColor('tab.selectedBorderTop', { - dark: TAB_ACTIVE_BORDER_TOP, - light: TAB_ACTIVE_BORDER_TOP, - hcDark: TAB_ACTIVE_BORDER_TOP, - hcLight: TAB_ACTIVE_BORDER_TOP -}, localize('tabSelectedBorderTop', "Border to the top of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); - -export const TAB_SELECTED_BACKGROUND = registerColor('tab.selectedBackground', { - dark: TAB_ACTIVE_BACKGROUND, - light: TAB_ACTIVE_BACKGROUND, - hcDark: TAB_ACTIVE_BACKGROUND, - hcLight: TAB_ACTIVE_BACKGROUND -}, localize('tabSelectedBackground', "Background of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); - -export const TAB_SELECTED_FOREGROUND = registerColor('tab.selectedForeground', { - dark: TAB_ACTIVE_FOREGROUND, - light: TAB_ACTIVE_FOREGROUND, - hcDark: TAB_ACTIVE_FOREGROUND, - hcLight: TAB_ACTIVE_FOREGROUND -}, localize('tabSelectedForeground', "Foreground of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_SELECTED_BORDER_TOP = registerColor('tab.selectedBorderTop', TAB_ACTIVE_BORDER_TOP, localize('tabSelectedBorderTop', "Border to the top of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_SELECTED_BACKGROUND = registerColor('tab.selectedBackground', TAB_ACTIVE_BACKGROUND, localize('tabSelectedBackground', "Background of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_HOVER_BORDER = registerColor('tab.hoverBorder', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('tabHoverBorder', "Border to highlight tabs when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_SELECTED_FOREGROUND = registerColor('tab.selectedForeground', TAB_ACTIVE_FOREGROUND, localize('tabSelectedForeground', "Foreground of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); + + +export const TAB_HOVER_BORDER = registerColor('tab.hoverBorder', null, localize('tabHoverBorder', "Border to highlight tabs when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_HOVER_BORDER = registerColor('tab.unfocusedHoverBorder', { dark: transparent(TAB_HOVER_BORDER, 0.5), @@ -272,19 +222,9 @@ export const TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER = registerColor('tab.unfocus // < --- Editors --- > -export const EDITOR_PANE_BACKGROUND = registerColor('editorPane.background', { - dark: editorBackground, - light: editorBackground, - hcDark: editorBackground, - hcLight: editorBackground -}, localize('editorPaneBackground', "Background color of the editor pane visible on the left and right side of the centered editor layout.")); +export const EDITOR_PANE_BACKGROUND = registerColor('editorPane.background', editorBackground, localize('editorPaneBackground', "Background color of the editor pane visible on the left and right side of the centered editor layout.")); -export const EDITOR_GROUP_EMPTY_BACKGROUND = registerColor('editorGroup.emptyBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('editorGroupEmptyBackground', "Background color of an empty editor group. Editor groups are the containers of editors.")); +export const EDITOR_GROUP_EMPTY_BACKGROUND = registerColor('editorGroup.emptyBackground', null, localize('editorGroupEmptyBackground', "Background color of an empty editor group. Editor groups are the containers of editors.")); export const EDITOR_GROUP_FOCUSED_EMPTY_BORDER = registerColor('editorGroup.focusedEmptyBorder', { dark: null, @@ -300,19 +240,9 @@ export const EDITOR_GROUP_HEADER_TABS_BACKGROUND = registerColor('editorGroupHea hcLight: null }, localize('tabsContainerBackground', "Background color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.")); -export const EDITOR_GROUP_HEADER_TABS_BORDER = registerColor('editorGroupHeader.tabsBorder', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('tabsContainerBorder', "Border color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.")); +export const EDITOR_GROUP_HEADER_TABS_BORDER = registerColor('editorGroupHeader.tabsBorder', null, localize('tabsContainerBorder', "Border color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.")); -export const EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND = registerColor('editorGroupHeader.noTabsBackground', { - dark: editorBackground, - light: editorBackground, - hcDark: editorBackground, - hcLight: editorBackground -}, localize('editorGroupHeaderBackground', "Background color of the editor group title header when (`\"workbench.editor.showTabs\": \"single\"`). Editor groups are the containers of editors.")); +export const EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND = registerColor('editorGroupHeader.noTabsBackground', editorBackground, localize('editorGroupHeaderBackground', "Background color of the editor group title header when (`\"workbench.editor.showTabs\": \"single\"`). Editor groups are the containers of editors.")); export const EDITOR_GROUP_HEADER_BORDER = registerColor('editorGroupHeader.border', { dark: null, @@ -335,19 +265,9 @@ export const EDITOR_DRAG_AND_DROP_BACKGROUND = registerColor('editorGroup.dropBa hcLight: Color.fromHex('#0F4A85').transparent(0.50) }, localize('editorDragAndDropBackground', "Background color when dragging editors around. The color should have transparency so that the editor contents can still shine through.")); -export const EDITOR_DROP_INTO_PROMPT_FOREGROUND = registerColor('editorGroup.dropIntoPromptForeground', { - dark: editorWidgetForeground, - light: editorWidgetForeground, - hcDark: editorWidgetForeground, - hcLight: editorWidgetForeground -}, localize('editorDropIntoPromptForeground', "Foreground color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); +export const EDITOR_DROP_INTO_PROMPT_FOREGROUND = registerColor('editorGroup.dropIntoPromptForeground', editorWidgetForeground, localize('editorDropIntoPromptForeground', "Foreground color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); -export const EDITOR_DROP_INTO_PROMPT_BACKGROUND = registerColor('editorGroup.dropIntoPromptBackground', { - dark: editorWidgetBackground, - light: editorWidgetBackground, - hcDark: editorWidgetBackground, - hcLight: editorWidgetBackground -}, localize('editorDropIntoPromptBackground', "Background color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); +export const EDITOR_DROP_INTO_PROMPT_BACKGROUND = registerColor('editorGroup.dropIntoPromptBackground', editorWidgetBackground, localize('editorDropIntoPromptBackground', "Background color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); export const EDITOR_DROP_INTO_PROMPT_BORDER = registerColor('editorGroup.dropIntoPromptBorder', { dark: null, @@ -356,19 +276,9 @@ export const EDITOR_DROP_INTO_PROMPT_BORDER = registerColor('editorGroup.dropInt hcLight: contrastBorder }, localize('editorDropIntoPromptBorder', "Border color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); -export const SIDE_BY_SIDE_EDITOR_HORIZONTAL_BORDER = registerColor('sideBySideEditor.horizontalBorder', { - dark: EDITOR_GROUP_BORDER, - light: EDITOR_GROUP_BORDER, - hcDark: EDITOR_GROUP_BORDER, - hcLight: EDITOR_GROUP_BORDER -}, localize('sideBySideEditor.horizontalBorder', "Color to separate two editors from each other when shown side by side in an editor group from top to bottom.")); +export const SIDE_BY_SIDE_EDITOR_HORIZONTAL_BORDER = registerColor('sideBySideEditor.horizontalBorder', EDITOR_GROUP_BORDER, localize('sideBySideEditor.horizontalBorder', "Color to separate two editors from each other when shown side by side in an editor group from top to bottom.")); -export const SIDE_BY_SIDE_EDITOR_VERTICAL_BORDER = registerColor('sideBySideEditor.verticalBorder', { - dark: EDITOR_GROUP_BORDER, - light: EDITOR_GROUP_BORDER, - hcDark: EDITOR_GROUP_BORDER, - hcLight: EDITOR_GROUP_BORDER -}, localize('sideBySideEditor.verticalBorder', "Color to separate two editors from each other when shown side by side in an editor group from left to right.")); +export const SIDE_BY_SIDE_EDITOR_VERTICAL_BORDER = registerColor('sideBySideEditor.verticalBorder', EDITOR_GROUP_BORDER, localize('sideBySideEditor.verticalBorder', "Color to separate two editors from each other when shown side by side in an editor group from left to right.")); // < --- Panels --- > @@ -382,12 +292,7 @@ export const PANEL_HEADER_BACKGROUND = registerColor('panel.headerBackground', { }, localize('panel.headerBackground', "Panel header background color. Panels are shown below the editor area and contain views like output and integrated terminal.")); // --- End Positron --- -export const PANEL_BACKGROUND = registerColor('panel.background', { - dark: editorBackground, - light: editorBackground, - hcDark: editorBackground, - hcLight: editorBackground -}, localize('panelBackground', "Panel background color. Panels are shown below the editor area and contain views like output and integrated terminal.")); +export const PANEL_BACKGROUND = registerColor('panel.background', editorBackground, localize('panelBackground', "Panel background color. Panels are shown below the editor area and contain views like output and integrated terminal.")); export const PANEL_BORDER = registerColor('panel.border', { dark: Color.fromHex('#808080').transparent(0.35), @@ -424,19 +329,9 @@ export const PANEL_INPUT_BORDER = registerColor('panelInput.border', { hcLight: inputBorder }, localize('panelInputBorder', "Input box border for inputs in the panel.")); -export const PANEL_DRAG_AND_DROP_BORDER = registerColor('panel.dropBorder', { - dark: PANEL_ACTIVE_TITLE_FOREGROUND, - light: PANEL_ACTIVE_TITLE_FOREGROUND, - hcDark: PANEL_ACTIVE_TITLE_FOREGROUND, - hcLight: PANEL_ACTIVE_TITLE_FOREGROUND -}, localize('panelDragAndDropBorder', "Drag and drop feedback color for the panel titles. Panels are shown below the editor area and contain views like output and integrated terminal.")); +export const PANEL_DRAG_AND_DROP_BORDER = registerColor('panel.dropBorder', PANEL_ACTIVE_TITLE_FOREGROUND, localize('panelDragAndDropBorder', "Drag and drop feedback color for the panel titles. Panels are shown below the editor area and contain views like output and integrated terminal.")); -export const PANEL_SECTION_DRAG_AND_DROP_BACKGROUND = registerColor('panelSection.dropBackground', { - dark: EDITOR_DRAG_AND_DROP_BACKGROUND, - light: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcDark: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcLight: EDITOR_DRAG_AND_DROP_BACKGROUND -}, localize('panelSectionDragAndDropBackground', "Drag and drop feedback color for the panel sections. The color should have transparency so that the panel sections can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +export const PANEL_SECTION_DRAG_AND_DROP_BACKGROUND = registerColor('panelSection.dropBackground', EDITOR_DRAG_AND_DROP_BACKGROUND, localize('panelSectionDragAndDropBackground', "Drag and drop feedback color for the panel sections. The color should have transparency so that the panel sections can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_HEADER_BACKGROUND = registerColor('panelSectionHeader.background', { dark: Color.fromHex('#808080').transparent(0.2), @@ -445,64 +340,24 @@ export const PANEL_SECTION_HEADER_BACKGROUND = registerColor('panelSectionHeader hcLight: null, }, localize('panelSectionHeaderBackground', "Panel section header background color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); -export const PANEL_SECTION_HEADER_FOREGROUND = registerColor('panelSectionHeader.foreground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('panelSectionHeaderForeground', "Panel section header foreground color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +export const PANEL_SECTION_HEADER_FOREGROUND = registerColor('panelSectionHeader.foreground', null, localize('panelSectionHeaderForeground', "Panel section header foreground color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); -export const PANEL_SECTION_HEADER_BORDER = registerColor('panelSectionHeader.border', { - dark: contrastBorder, - light: contrastBorder, - hcDark: contrastBorder, - hcLight: contrastBorder -}, localize('panelSectionHeaderBorder', "Panel section header border color used when multiple views are stacked vertically in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +export const PANEL_SECTION_HEADER_BORDER = registerColor('panelSectionHeader.border', contrastBorder, localize('panelSectionHeaderBorder', "Panel section header border color used when multiple views are stacked vertically in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); -export const PANEL_SECTION_BORDER = registerColor('panelSection.border', { - dark: PANEL_BORDER, - light: PANEL_BORDER, - hcDark: PANEL_BORDER, - hcLight: PANEL_BORDER -}, localize('panelSectionBorder', "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +export const PANEL_SECTION_BORDER = registerColor('panelSection.border', PANEL_BORDER, localize('panelSectionBorder', "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); -export const PANEL_STICKY_SCROLL_BACKGROUND = registerColor('panelStickyScroll.background', { - dark: PANEL_BACKGROUND, - light: PANEL_BACKGROUND, - hcDark: PANEL_BACKGROUND, - hcLight: PANEL_BACKGROUND -}, localize('panelStickyScrollBackground', "Background color of sticky scroll in the panel.")); +export const PANEL_STICKY_SCROLL_BACKGROUND = registerColor('panelStickyScroll.background', PANEL_BACKGROUND, localize('panelStickyScrollBackground', "Background color of sticky scroll in the panel.")); -export const PANEL_STICKY_SCROLL_BORDER = registerColor('panelStickyScroll.border', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('panelStickyScrollBorder', "Border color of sticky scroll in the panel.")); +export const PANEL_STICKY_SCROLL_BORDER = registerColor('panelStickyScroll.border', null, localize('panelStickyScrollBorder', "Border color of sticky scroll in the panel.")); -export const PANEL_STICKY_SCROLL_SHADOW = registerColor('panelStickyScroll.shadow', { - dark: scrollbarShadow, - light: scrollbarShadow, - hcDark: scrollbarShadow, - hcLight: scrollbarShadow -}, localize('panelStickyScrollShadow', "Shadow color of sticky scroll in the panel.")); +export const PANEL_STICKY_SCROLL_SHADOW = registerColor('panelStickyScroll.shadow', scrollbarShadow, localize('panelStickyScrollShadow', "Shadow color of sticky scroll in the panel.")); // < --- Output Editor --> -const OUTPUT_VIEW_BACKGROUND = registerColor('outputView.background', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('outputViewBackground', "Output view background color.")); +const OUTPUT_VIEW_BACKGROUND = registerColor('outputView.background', null, localize('outputViewBackground', "Output view background color.")); -registerColor('outputViewStickyScroll.background', { - dark: OUTPUT_VIEW_BACKGROUND, - light: OUTPUT_VIEW_BACKGROUND, - hcDark: OUTPUT_VIEW_BACKGROUND, - hcLight: OUTPUT_VIEW_BACKGROUND -}, localize('outputViewStickyScrollBackground', "Output view sticky scroll background color.")); +registerColor('outputViewStickyScroll.background', OUTPUT_VIEW_BACKGROUND, localize('outputViewStickyScrollBackground', "Output view sticky scroll background color.")); // < --- Banner --- > @@ -514,19 +369,9 @@ export const BANNER_BACKGROUND = registerColor('banner.background', { hcLight: listActiveSelectionBackground }, localize('banner.background', "Banner background color. The banner is shown under the title bar of the window.")); -export const BANNER_FOREGROUND = registerColor('banner.foreground', { - dark: listActiveSelectionForeground, - light: listActiveSelectionForeground, - hcDark: listActiveSelectionForeground, - hcLight: listActiveSelectionForeground -}, localize('banner.foreground', "Banner foreground color. The banner is shown under the title bar of the window.")); +export const BANNER_FOREGROUND = registerColor('banner.foreground', listActiveSelectionForeground, localize('banner.foreground', "Banner foreground color. The banner is shown under the title bar of the window.")); -export const BANNER_ICON_FOREGROUND = registerColor('banner.iconForeground', { - dark: editorInfoForeground, - light: editorInfoForeground, - hcDark: editorInfoForeground, - hcLight: editorInfoForeground -}, localize('banner.iconForeground', "Banner icon color. The banner is shown under the title bar of the window.")); +export const BANNER_ICON_FOREGROUND = registerColor('banner.iconForeground', editorInfoForeground, localize('banner.iconForeground', "Banner icon color. The banner is shown under the title bar of the window.")); // < --- Status --- > @@ -537,12 +382,7 @@ export const STATUS_BAR_FOREGROUND = registerColor('statusBar.foreground', { hcLight: editorForeground }, localize('statusBarForeground', "Status bar foreground color when a workspace or folder is opened. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_NO_FOLDER_FOREGROUND = registerColor('statusBar.noFolderForeground', { - dark: STATUS_BAR_FOREGROUND, - light: STATUS_BAR_FOREGROUND, - hcDark: STATUS_BAR_FOREGROUND, - hcLight: STATUS_BAR_FOREGROUND -}, localize('statusBarNoFolderForeground', "Status bar foreground color when no folder is opened. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_NO_FOLDER_FOREGROUND = registerColor('statusBar.noFolderForeground', STATUS_BAR_FOREGROUND, localize('statusBarNoFolderForeground', "Status bar foreground color when no folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_BACKGROUND = registerColor('statusBar.background', { dark: '#007ACC', @@ -572,12 +412,7 @@ export const STATUS_BAR_FOCUS_BORDER = registerColor('statusBar.focusBorder', { hcLight: STATUS_BAR_FOREGROUND }, localize('statusBarFocusBorder', "Status bar border color when focused on keyboard navigation. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_NO_FOLDER_BORDER = registerColor('statusBar.noFolderBorder', { - dark: STATUS_BAR_BORDER, - light: STATUS_BAR_BORDER, - hcDark: STATUS_BAR_BORDER, - hcLight: STATUS_BAR_BORDER -}, localize('statusBarNoFolderBorder', "Status bar border color separating to the sidebar and editor when no folder is opened. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_NO_FOLDER_BORDER = registerColor('statusBar.noFolderBorder', STATUS_BAR_BORDER, localize('statusBarNoFolderBorder', "Status bar border color separating to the sidebar and editor when no folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ITEM_ACTIVE_BACKGROUND = registerColor('statusBarItem.activeBackground', { dark: Color.white.transparent(0.18), @@ -600,12 +435,7 @@ export const STATUS_BAR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.hov hcLight: Color.black.transparent(0.12) }, localize('statusBarItemHoverBackground', "Status bar item background color when hovering. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.hoverForeground', { - dark: STATUS_BAR_FOREGROUND, - light: STATUS_BAR_FOREGROUND, - hcDark: STATUS_BAR_FOREGROUND, - hcLight: STATUS_BAR_FOREGROUND -}, localize('statusBarItemHoverForeground', "Status bar item foreground color when hovering. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.hoverForeground', STATUS_BAR_FOREGROUND, localize('statusBarItemHoverForeground', "Status bar item foreground color when hovering. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND = registerColor('statusBarItem.compactHoverBackground', { dark: Color.white.transparent(0.20), @@ -614,26 +444,11 @@ export const STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND = registerColor('statusBar hcLight: Color.black.transparent(0.20) }, localize('statusBarItemCompactHoverBackground', "Status bar item background color when hovering an item that contains two hovers. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_PROMINENT_ITEM_FOREGROUND = registerColor('statusBarItem.prominentForeground', { - dark: STATUS_BAR_FOREGROUND, - light: STATUS_BAR_FOREGROUND, - hcDark: STATUS_BAR_FOREGROUND, - hcLight: STATUS_BAR_FOREGROUND -}, localize('statusBarProminentItemForeground', "Status bar prominent items foreground color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); - -export const STATUS_BAR_PROMINENT_ITEM_BACKGROUND = registerColor('statusBarItem.prominentBackground', { - dark: Color.black.transparent(0.5), - light: Color.black.transparent(0.5), - hcDark: Color.black.transparent(0.5), - hcLight: Color.black.transparent(0.5), -}, localize('statusBarProminentItemBackground', "Status bar prominent items background color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); - -export const STATUS_BAR_PROMINENT_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.prominentHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarProminentItemHoverForeground', "Status bar prominent items foreground color when hovering. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_PROMINENT_ITEM_FOREGROUND = registerColor('statusBarItem.prominentForeground', STATUS_BAR_FOREGROUND, localize('statusBarProminentItemForeground', "Status bar prominent items foreground color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); + +export const STATUS_BAR_PROMINENT_ITEM_BACKGROUND = registerColor('statusBarItem.prominentBackground', Color.black.transparent(0.5), localize('statusBarProminentItemBackground', "Status bar prominent items background color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); + +export const STATUS_BAR_PROMINENT_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.prominentHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarProminentItemHoverForeground', "Status bar prominent items foreground color when hovering. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.prominentHoverBackground', { dark: Color.black.transparent(0.3), @@ -649,26 +464,11 @@ export const STATUS_BAR_ERROR_ITEM_BACKGROUND = registerColor('statusBarItem.err hcLight: '#B5200D' }, localize('statusBarErrorItemBackground', "Status bar error items background color. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_ERROR_ITEM_FOREGROUND = registerColor('statusBarItem.errorForeground', { - dark: Color.white, - light: Color.white, - hcDark: Color.white, - hcLight: Color.white -}, localize('statusBarErrorItemForeground', "Status bar error items foreground color. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ERROR_ITEM_FOREGROUND = registerColor('statusBarItem.errorForeground', Color.white, localize('statusBarErrorItemForeground', "Status bar error items foreground color. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_ERROR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.errorHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarErrorItemHoverForeground', "Status bar error items foreground color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ERROR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.errorHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarErrorItemHoverForeground', "Status bar error items foreground color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_ERROR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.errorHoverBackground', { - dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, - light: STATUS_BAR_ITEM_HOVER_BACKGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_BACKGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_BACKGROUND -}, localize('statusBarErrorItemHoverBackground', "Status bar error items background color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ERROR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.errorHoverBackground', STATUS_BAR_ITEM_HOVER_BACKGROUND, localize('statusBarErrorItemHoverBackground', "Status bar error items background color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_WARNING_ITEM_BACKGROUND = registerColor('statusBarItem.warningBackground', { dark: darken(editorWarningForeground, .4), @@ -677,26 +477,11 @@ export const STATUS_BAR_WARNING_ITEM_BACKGROUND = registerColor('statusBarItem.w hcLight: '#895503' }, localize('statusBarWarningItemBackground', "Status bar warning items background color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_WARNING_ITEM_FOREGROUND = registerColor('statusBarItem.warningForeground', { - dark: Color.white, - light: Color.white, - hcDark: Color.white, - hcLight: Color.white -}, localize('statusBarWarningItemForeground', "Status bar warning items foreground color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_WARNING_ITEM_FOREGROUND = registerColor('statusBarItem.warningForeground', Color.white, localize('statusBarWarningItemForeground', "Status bar warning items foreground color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_WARNING_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.warningHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarWarningItemHoverForeground', "Status bar warning items foreground color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_WARNING_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.warningHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarWarningItemHoverForeground', "Status bar warning items foreground color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_WARNING_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.warningHoverBackground', { - dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, - light: STATUS_BAR_ITEM_HOVER_BACKGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_BACKGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_BACKGROUND -}, localize('statusBarWarningItemHoverBackground', "Status bar warning items background color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_WARNING_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.warningHoverBackground', STATUS_BAR_ITEM_HOVER_BACKGROUND, localize('statusBarWarningItemHoverBackground', "Status bar warning items background color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); // < --- Activity Bar --- > @@ -743,12 +528,7 @@ export const ACTIVITY_BAR_ACTIVE_FOCUS_BORDER = registerColor('activityBar.activ hcLight: '#B5200D' }, localize('activityBarActiveFocusBorder', "Activity bar focus border color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_ACTIVE_BACKGROUND = registerColor('activityBar.activeBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('activityBarActiveBackground', "Activity bar background color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_ACTIVE_BACKGROUND = registerColor('activityBar.activeBackground', null, localize('activityBarActiveBackground', "Activity bar background color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_DRAG_AND_DROP_BORDER = registerColor('activityBar.dropBorder', { dark: ACTIVITY_BAR_FOREGROUND, @@ -764,12 +544,7 @@ export const ACTIVITY_BAR_BADGE_BACKGROUND = registerColor('activityBarBadge.bac hcLight: '#0F4A85' }, localize('activityBarBadgeBackground', "Activity notification badge background color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_BADGE_FOREGROUND = registerColor('activityBarBadge.foreground', { - dark: Color.white, - light: Color.white, - hcDark: Color.white, - hcLight: Color.white -}, localize('activityBarBadgeForeground', "Activity notification badge foreground color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_BADGE_FOREGROUND = registerColor('activityBarBadge.foreground', Color.white, localize('activityBarBadgeForeground', "Activity notification badge foreground color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_TOP_FOREGROUND = registerColor('activityBarTop.foreground', { dark: '#E7E7E7', @@ -785,12 +560,7 @@ export const ACTIVITY_BAR_TOP_ACTIVE_BORDER = registerColor('activityBarTop.acti hcLight: '#B5200D' }, localize('activityBarTopActiveFocusBorder', "Focus border color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_TOP_ACTIVE_BACKGROUND = registerColor('activityBarTop.activeBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('activityBarTopActiveBackground', "Background color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_TOP_ACTIVE_BACKGROUND = registerColor('activityBarTop.activeBackground', null, localize('activityBarTopActiveBackground', "Background color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); export const ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND = registerColor('activityBarTop.inactiveForeground', { dark: transparent(ACTIVITY_BAR_TOP_FOREGROUND, 0.6), @@ -799,19 +569,9 @@ export const ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND = registerColor('activityBarTo hcLight: editorForeground }, localize('activityBarTopInActiveForeground', "Inactive foreground color of the item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER = registerColor('activityBarTop.dropBorder', { - dark: ACTIVITY_BAR_TOP_FOREGROUND, - light: ACTIVITY_BAR_TOP_FOREGROUND, - hcDark: ACTIVITY_BAR_TOP_FOREGROUND, - hcLight: ACTIVITY_BAR_TOP_FOREGROUND -}, localize('activityBarTopDragAndDropBorder', "Drag and drop feedback color for the items in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER = registerColor('activityBarTop.dropBorder', ACTIVITY_BAR_TOP_FOREGROUND, localize('activityBarTopDragAndDropBorder', "Drag and drop feedback color for the items in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_TOP_BACKGROUND = registerColor('activityBarTop.background', { - dark: null, - light: null, - hcDark: null, - hcLight: null, -}, localize('activityBarTopBackground', "Background color of the activity bar when set to top / bottom.")); +export const ACTIVITY_BAR_TOP_BACKGROUND = registerColor('activityBarTop.background', null, localize('activityBarTopBackground', "Background color of the activity bar when set to top / bottom.")); // < --- Profiles --- > @@ -832,26 +592,11 @@ export const PROFILE_BADGE_FOREGROUND = registerColor('profileBadge.foreground', // < --- Remote --- > -export const STATUS_BAR_REMOTE_ITEM_BACKGROUND = registerColor('statusBarItem.remoteBackground', { - dark: ACTIVITY_BAR_BADGE_BACKGROUND, - light: ACTIVITY_BAR_BADGE_BACKGROUND, - hcDark: ACTIVITY_BAR_BADGE_BACKGROUND, - hcLight: ACTIVITY_BAR_BADGE_BACKGROUND -}, localize('statusBarItemHostBackground', "Background color for the remote indicator on the status bar.")); - -export const STATUS_BAR_REMOTE_ITEM_FOREGROUND = registerColor('statusBarItem.remoteForeground', { - dark: ACTIVITY_BAR_BADGE_FOREGROUND, - light: ACTIVITY_BAR_BADGE_FOREGROUND, - hcDark: ACTIVITY_BAR_BADGE_FOREGROUND, - hcLight: ACTIVITY_BAR_BADGE_FOREGROUND -}, localize('statusBarItemHostForeground', "Foreground color for the remote indicator on the status bar.")); - -export const STATUS_BAR_REMOTE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.remoteHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarRemoteItemHoverForeground', "Foreground color for the remote indicator on the status bar when hovering.")); +export const STATUS_BAR_REMOTE_ITEM_BACKGROUND = registerColor('statusBarItem.remoteBackground', ACTIVITY_BAR_BADGE_BACKGROUND, localize('statusBarItemHostBackground', "Background color for the remote indicator on the status bar.")); + +export const STATUS_BAR_REMOTE_ITEM_FOREGROUND = registerColor('statusBarItem.remoteForeground', ACTIVITY_BAR_BADGE_FOREGROUND, localize('statusBarItemHostForeground', "Foreground color for the remote indicator on the status bar.")); + +export const STATUS_BAR_REMOTE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.remoteHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarRemoteItemHoverForeground', "Foreground color for the remote indicator on the status bar when hovering.")); export const STATUS_BAR_REMOTE_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.remoteHoverBackground', { dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, @@ -860,26 +605,11 @@ export const STATUS_BAR_REMOTE_ITEM_HOVER_BACKGROUND = registerColor('statusBarI hcLight: null }, localize('statusBarRemoteItemHoverBackground', "Background color for the remote indicator on the status bar when hovering.")); -export const STATUS_BAR_OFFLINE_ITEM_BACKGROUND = registerColor('statusBarItem.offlineBackground', { - dark: '#6c1717', - light: '#6c1717', - hcDark: '#6c1717', - hcLight: '#6c1717' -}, localize('statusBarItemOfflineBackground', "Status bar item background color when the workbench is offline.")); - -export const STATUS_BAR_OFFLINE_ITEM_FOREGROUND = registerColor('statusBarItem.offlineForeground', { - dark: STATUS_BAR_REMOTE_ITEM_FOREGROUND, - light: STATUS_BAR_REMOTE_ITEM_FOREGROUND, - hcDark: STATUS_BAR_REMOTE_ITEM_FOREGROUND, - hcLight: STATUS_BAR_REMOTE_ITEM_FOREGROUND -}, localize('statusBarItemOfflineForeground', "Status bar item foreground color when the workbench is offline.")); - -export const STATUS_BAR_OFFLINE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.offlineHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarOfflineItemHoverForeground', "Status bar item foreground hover color when the workbench is offline.")); +export const STATUS_BAR_OFFLINE_ITEM_BACKGROUND = registerColor('statusBarItem.offlineBackground', '#6c1717', localize('statusBarItemOfflineBackground', "Status bar item background color when the workbench is offline.")); + +export const STATUS_BAR_OFFLINE_ITEM_FOREGROUND = registerColor('statusBarItem.offlineForeground', STATUS_BAR_REMOTE_ITEM_FOREGROUND, localize('statusBarItemOfflineForeground', "Status bar item foreground color when the workbench is offline.")); + +export const STATUS_BAR_OFFLINE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.offlineHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarOfflineItemHoverForeground', "Status bar item foreground hover color when the workbench is offline.")); export const STATUS_BAR_OFFLINE_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.offlineHoverBackground', { dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, @@ -888,19 +618,9 @@ export const STATUS_BAR_OFFLINE_ITEM_HOVER_BACKGROUND = registerColor('statusBar hcLight: null }, localize('statusBarOfflineItemHoverBackground', "Status bar item background hover color when the workbench is offline.")); -export const EXTENSION_BADGE_REMOTE_BACKGROUND = registerColor('extensionBadge.remoteBackground', { - dark: ACTIVITY_BAR_BADGE_BACKGROUND, - light: ACTIVITY_BAR_BADGE_BACKGROUND, - hcDark: ACTIVITY_BAR_BADGE_BACKGROUND, - hcLight: ACTIVITY_BAR_BADGE_BACKGROUND -}, localize('extensionBadge.remoteBackground', "Background color for the remote badge in the extensions view.")); +export const EXTENSION_BADGE_REMOTE_BACKGROUND = registerColor('extensionBadge.remoteBackground', ACTIVITY_BAR_BADGE_BACKGROUND, localize('extensionBadge.remoteBackground', "Background color for the remote badge in the extensions view.")); -export const EXTENSION_BADGE_REMOTE_FOREGROUND = registerColor('extensionBadge.remoteForeground', { - dark: ACTIVITY_BAR_BADGE_FOREGROUND, - light: ACTIVITY_BAR_BADGE_FOREGROUND, - hcDark: ACTIVITY_BAR_BADGE_FOREGROUND, - hcLight: ACTIVITY_BAR_BADGE_FOREGROUND -}, localize('extensionBadge.remoteForeground', "Foreground color for the remote badge in the extensions view.")); +export const EXTENSION_BADGE_REMOTE_FOREGROUND = registerColor('extensionBadge.remoteForeground', ACTIVITY_BAR_BADGE_FOREGROUND, localize('extensionBadge.remoteForeground', "Foreground color for the remote badge in the extensions view.")); // < --- Side Bar --- > @@ -912,12 +632,7 @@ export const SIDE_BAR_BACKGROUND = registerColor('sideBar.background', { hcLight: '#FFFFFF' }, localize('sideBarBackground', "Side bar background color. The side bar is the container for views like explorer and search.")); -export const SIDE_BAR_FOREGROUND = registerColor('sideBar.foreground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('sideBarForeground', "Side bar foreground color. The side bar is the container for views like explorer and search.")); +export const SIDE_BAR_FOREGROUND = registerColor('sideBar.foreground', null, localize('sideBarForeground', "Side bar foreground color. The side bar is the container for views like explorer and search.")); export const SIDE_BAR_BORDER = registerColor('sideBar.border', { dark: null, @@ -926,26 +641,11 @@ export const SIDE_BAR_BORDER = registerColor('sideBar.border', { hcLight: contrastBorder }, localize('sideBarBorder', "Side bar border color on the side separating to the editor. The side bar is the container for views like explorer and search.")); -export const SIDE_BAR_TITLE_BACKGROUND = registerColor('sideBarTitle.background', { - dark: SIDE_BAR_BACKGROUND, - light: SIDE_BAR_BACKGROUND, - hcDark: SIDE_BAR_BACKGROUND, - hcLight: SIDE_BAR_BACKGROUND -}, localize('sideBarTitleBackground', "Side bar title background color. The side bar is the container for views like explorer and search.")); - -export const SIDE_BAR_TITLE_FOREGROUND = registerColor('sideBarTitle.foreground', { - dark: SIDE_BAR_FOREGROUND, - light: SIDE_BAR_FOREGROUND, - hcDark: SIDE_BAR_FOREGROUND, - hcLight: 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_DRAG_AND_DROP_BACKGROUND = registerColor('sideBar.dropBackground', { - dark: EDITOR_DRAG_AND_DROP_BACKGROUND, - light: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcDark: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcLight: 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_TITLE_BACKGROUND = registerColor('sideBarTitle.background', SIDE_BAR_BACKGROUND, localize('sideBarTitleBackground', "Side bar title background color. The side bar is the container for views like explorer and search.")); + +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_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', { dark: Color.fromHex('#808080').transparent(0.2), @@ -954,47 +654,17 @@ export const SIDE_BAR_SECTION_HEADER_BACKGROUND = registerColor('sideBarSectionH hcLight: null }, localize('sideBarSectionHeaderBackground', "Side bar section header background color. 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_FOREGROUND = registerColor('sideBarSectionHeader.foreground', { - dark: SIDE_BAR_FOREGROUND, - light: SIDE_BAR_FOREGROUND, - hcDark: SIDE_BAR_FOREGROUND, - hcLight: SIDE_BAR_FOREGROUND -}, localize('sideBarSectionHeaderForeground', "Side bar section header foreground color. 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_FOREGROUND = registerColor('sideBarSectionHeader.foreground', SIDE_BAR_FOREGROUND, localize('sideBarSectionHeaderForeground', "Side bar section header foreground color. 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_BORDER = registerColor('sideBarSectionHeader.border', { - dark: contrastBorder, - light: contrastBorder, - hcDark: contrastBorder, - hcLight: contrastBorder -}, localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); - -export const ACTIVITY_BAR_TOP_BORDER = registerColor('sideBarActivityBarTop.border', { - dark: SIDE_BAR_SECTION_HEADER_BORDER, - light: SIDE_BAR_SECTION_HEADER_BORDER, - hcDark: SIDE_BAR_SECTION_HEADER_BORDER, - hcLight: SIDE_BAR_SECTION_HEADER_BORDER -}, localize('sideBarActivityBarTopBorder', "Border color between the activity bar at the top/bottom and the views.")); - -export const SIDE_BAR_STICKY_SCROLL_BACKGROUND = registerColor('sideBarStickyScroll.background', { - dark: SIDE_BAR_BACKGROUND, - light: SIDE_BAR_BACKGROUND, - hcDark: SIDE_BAR_BACKGROUND, - hcLight: SIDE_BAR_BACKGROUND -}, localize('sideBarStickyScrollBackground', "Background color of sticky scroll in the side bar.")); - -export const SIDE_BAR_STICKY_SCROLL_BORDER = registerColor('sideBarStickyScroll.border', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('sideBarStickyScrollBorder', "Border color of sticky scroll in the side bar.")); +export const SIDE_BAR_SECTION_HEADER_BORDER = registerColor('sideBarSectionHeader.border', contrastBorder, localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); + +export const ACTIVITY_BAR_TOP_BORDER = registerColor('sideBarActivityBarTop.border', SIDE_BAR_SECTION_HEADER_BORDER, localize('sideBarActivityBarTopBorder', "Border color between the activity bar at the top/bottom and the views.")); + +export const SIDE_BAR_STICKY_SCROLL_BACKGROUND = registerColor('sideBarStickyScroll.background', SIDE_BAR_BACKGROUND, localize('sideBarStickyScrollBackground', "Background color of sticky scroll in the side bar.")); + +export const SIDE_BAR_STICKY_SCROLL_BORDER = registerColor('sideBarStickyScroll.border', null, localize('sideBarStickyScrollBorder', "Border color of sticky scroll in the side bar.")); -export const SIDE_BAR_STICKY_SCROLL_SHADOW = registerColor('sideBarStickyScroll.shadow', { - dark: scrollbarShadow, - light: scrollbarShadow, - hcDark: scrollbarShadow, - hcLight: scrollbarShadow -}, localize('sideBarStickyScrollShadow', "Shadow color of sticky scroll in the side bar.")); +export const SIDE_BAR_STICKY_SCROLL_SHADOW = registerColor('sideBarStickyScroll.shadow', scrollbarShadow, localize('sideBarStickyScrollShadow', "Shadow color of sticky scroll in the side bar.")); // < --- Title Bar --- > @@ -1035,12 +705,7 @@ export const TITLE_BAR_BORDER = registerColor('titleBar.border', { // < --- Menubar --- > -export const MENUBAR_SELECTION_FOREGROUND = registerColor('menubar.selectionForeground', { - dark: TITLE_BAR_ACTIVE_FOREGROUND, - light: TITLE_BAR_ACTIVE_FOREGROUND, - hcDark: TITLE_BAR_ACTIVE_FOREGROUND, - hcLight: TITLE_BAR_ACTIVE_FOREGROUND, -}, localize('menubarSelectionForeground', "Foreground color of the selected menu item in the menubar.")); +export const MENUBAR_SELECTION_FOREGROUND = registerColor('menubar.selectionForeground', TITLE_BAR_ACTIVE_FOREGROUND, localize('menubarSelectionForeground', "Foreground color of the selected menu item in the menubar.")); export const MENUBAR_SELECTION_BACKGROUND = registerColor('menubar.selectionBackground', { dark: toolbarHoverBackground, @@ -1061,19 +726,19 @@ export const MENUBAR_SELECTION_BORDER = registerColor('menubar.selectionBorder', // foreground (inactive and active) export const COMMAND_CENTER_FOREGROUND = registerColor( 'commandCenter.foreground', - { dark: TITLE_BAR_ACTIVE_FOREGROUND, hcDark: TITLE_BAR_ACTIVE_FOREGROUND, light: TITLE_BAR_ACTIVE_FOREGROUND, hcLight: TITLE_BAR_ACTIVE_FOREGROUND }, + TITLE_BAR_ACTIVE_FOREGROUND, localize('commandCenter-foreground', "Foreground color of the command center"), false ); export const COMMAND_CENTER_ACTIVEFOREGROUND = registerColor( 'commandCenter.activeForeground', - { dark: MENUBAR_SELECTION_FOREGROUND, hcDark: MENUBAR_SELECTION_FOREGROUND, light: MENUBAR_SELECTION_FOREGROUND, hcLight: MENUBAR_SELECTION_FOREGROUND }, + MENUBAR_SELECTION_FOREGROUND, localize('commandCenter-activeForeground', "Active foreground color of the command center"), false ); export const COMMAND_CENTER_INACTIVEFOREGROUND = registerColor( 'commandCenter.inactiveForeground', - { dark: TITLE_BAR_INACTIVE_FOREGROUND, hcDark: TITLE_BAR_INACTIVE_FOREGROUND, light: TITLE_BAR_INACTIVE_FOREGROUND, hcLight: TITLE_BAR_INACTIVE_FOREGROUND }, + TITLE_BAR_INACTIVE_FOREGROUND, localize('commandCenter-inactiveForeground', "Foreground color of the command center when the window is inactive"), false ); @@ -1103,7 +768,7 @@ export const COMMAND_CENTER_ACTIVEBORDER = registerColor( ); // border: defaults to active background export const COMMAND_CENTER_INACTIVEBORDER = registerColor( - 'commandCenter.inactiveBorder', { dark: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), hcDark: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), light: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), hcLight: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25) }, + 'commandCenter.inactiveBorder', transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), localize('commandCenter-inactiveBorder', "Border color of the command center when the window is inactive"), false ); @@ -1125,33 +790,13 @@ export const NOTIFICATIONS_TOAST_BORDER = registerColor('notificationToast.borde hcLight: contrastBorder }, localize('notificationToastBorder', "Notification toast border color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_FOREGROUND = registerColor('notifications.foreground', { - dark: editorWidgetForeground, - light: editorWidgetForeground, - hcDark: editorWidgetForeground, - hcLight: editorWidgetForeground -}, localize('notificationsForeground', "Notifications foreground color. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_FOREGROUND = registerColor('notifications.foreground', editorWidgetForeground, localize('notificationsForeground', "Notifications foreground color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_BACKGROUND = registerColor('notifications.background', { - dark: editorWidgetBackground, - light: editorWidgetBackground, - hcDark: editorWidgetBackground, - hcLight: editorWidgetBackground -}, localize('notificationsBackground', "Notifications background color. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_BACKGROUND = registerColor('notifications.background', editorWidgetBackground, localize('notificationsBackground', "Notifications background color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_LINKS = registerColor('notificationLink.foreground', { - dark: textLinkForeground, - light: textLinkForeground, - hcDark: textLinkForeground, - hcLight: textLinkForeground -}, localize('notificationsLink', "Notification links foreground color. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_LINKS = registerColor('notificationLink.foreground', textLinkForeground, localize('notificationsLink', "Notification links foreground color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_CENTER_HEADER_FOREGROUND = registerColor('notificationCenterHeader.foreground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('notificationCenterHeaderForeground', "Notifications center header foreground color. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_CENTER_HEADER_FOREGROUND = registerColor('notificationCenterHeader.foreground', null, localize('notificationCenterHeaderForeground', "Notifications center header foreground color. Notifications slide in from the bottom right of the window.")); export const NOTIFICATIONS_CENTER_HEADER_BACKGROUND = registerColor('notificationCenterHeader.background', { dark: lighten(NOTIFICATIONS_BACKGROUND, 0.3), @@ -1160,33 +805,13 @@ export const NOTIFICATIONS_CENTER_HEADER_BACKGROUND = registerColor('notificatio hcLight: NOTIFICATIONS_BACKGROUND }, localize('notificationCenterHeaderBackground', "Notifications center header background color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_BORDER = registerColor('notifications.border', { - dark: NOTIFICATIONS_CENTER_HEADER_BACKGROUND, - light: NOTIFICATIONS_CENTER_HEADER_BACKGROUND, - hcDark: NOTIFICATIONS_CENTER_HEADER_BACKGROUND, - hcLight: NOTIFICATIONS_CENTER_HEADER_BACKGROUND -}, localize('notificationsBorder', "Notifications border color separating from other notifications in the notifications center. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_BORDER = registerColor('notifications.border', NOTIFICATIONS_CENTER_HEADER_BACKGROUND, localize('notificationsBorder', "Notifications border color separating from other notifications in the notifications center. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_ERROR_ICON_FOREGROUND = registerColor('notificationsErrorIcon.foreground', { - dark: editorErrorForeground, - light: editorErrorForeground, - hcDark: editorErrorForeground, - hcLight: editorErrorForeground -}, localize('notificationsErrorIconForeground', "The color used for the icon of error notifications. Notifications slide in from the bottom right of the window.")); - -export const NOTIFICATIONS_WARNING_ICON_FOREGROUND = registerColor('notificationsWarningIcon.foreground', { - dark: editorWarningForeground, - light: editorWarningForeground, - hcDark: editorWarningForeground, - hcLight: editorWarningForeground -}, localize('notificationsWarningIconForeground', "The color used for the icon of warning notifications. Notifications slide in from the bottom right of the window.")); - -export const NOTIFICATIONS_INFO_ICON_FOREGROUND = registerColor('notificationsInfoIcon.foreground', { - dark: editorInfoForeground, - light: editorInfoForeground, - hcDark: editorInfoForeground, - hcLight: editorInfoForeground -}, localize('notificationsInfoIconForeground', "The color used for the icon of info notifications. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_ERROR_ICON_FOREGROUND = registerColor('notificationsErrorIcon.foreground', editorErrorForeground, localize('notificationsErrorIconForeground', "The color used for the icon of error notifications. Notifications slide in from the bottom right of the window.")); + +export const NOTIFICATIONS_WARNING_ICON_FOREGROUND = registerColor('notificationsWarningIcon.foreground', editorWarningForeground, localize('notificationsWarningIconForeground', "The color used for the icon of warning notifications. Notifications slide in from the bottom right of the window.")); + +export const NOTIFICATIONS_INFO_ICON_FOREGROUND = registerColor('notificationsInfoIcon.foreground', editorInfoForeground, localize('notificationsInfoIconForeground', "The color used for the icon of info notifications. Notifications slide in from the bottom right of the window.")); export const WINDOW_ACTIVE_BORDER = registerColor('window.activeBorder', { dark: null, diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 1d163de1af4..fa4e47467d2 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -56,6 +56,7 @@ export const enum AccessibilityVerbositySettingId { Hover = 'accessibility.verbosity.hover', Notification = 'accessibility.verbosity.notification', EmptyEditorHint = 'accessibility.verbosity.emptyEditorHint', + ReplInputHint = 'accessibility.verbosity.replInputHint', Comments = 'accessibility.verbosity.comments', DiffEditorActive = 'accessibility.verbosity.diffEditorActive' } @@ -158,6 +159,10 @@ const configuration: IConfigurationNode = { description: localize('verbosity.emptyEditorHint', 'Provide information about relevant actions in an empty text editor.'), ...baseVerbosityProperty }, + [AccessibilityVerbositySettingId.ReplInputHint]: { + description: localize('verbosity.replInputHint', 'Provide information about relevant actions For the Repl input.'), + ...baseVerbosityProperty + }, [AccessibilityVerbositySettingId.Comments]: { description: localize('verbosity.comments', 'Provide information about actions that can be taken in the comment widget or in a file which contains comments.'), ...baseVerbosityProperty @@ -171,118 +176,77 @@ const configuration: IConfigurationNode = { type: 'boolean', default: true }, - 'accessibility.signalOptions': { - description: 'Configures the behavior of signals (audio cues and announcements) in the workbench. Includes volume, debounce position changes, and delays for different types of signals.', - type: 'object', - additionalProperties: false, - properties: { - 'volume': { - 'description': localize('accessibility.signalOptions.volume', "The volume of the sounds in percent (0-100)."), + 'accessibility.signalOptions.volume': { + 'description': localize('accessibility.signalOptions.volume', "The volume of the sounds in percent (0-100)."), + 'type': 'number', + 'minimum': 0, + 'maximum': 100, + 'default': 70, + 'tags': ['accessibility'] + }, + 'accessibility.signalOptions.debouncePositionChanges': { + 'description': localize('accessibility.signalOptions.debouncePositionChanges', "Whether or not position changes should be debounced"), + 'type': 'boolean', + 'default': false, + 'tags': ['accessibility'] + }, + 'accessibility.signalOptions.experimental.delays.general': { + 'type': 'object', + 'description': 'Delays for all signals besides error and warning at position', + 'additionalProperties': false, + 'properties': { + 'announcement': { + 'description': localize('accessibility.signalOptions.delays.general.announcement', "The delay in milliseconds before an announcement is made."), 'type': 'number', 'minimum': 0, - 'maximum': 100, - 'default': 70, + 'default': 3000 }, - 'debouncePositionChanges': { - 'description': localize('accessibility.signalOptions.debouncePositionChanges', "Whether or not position changes should be debounced"), - 'type': 'boolean', - 'default': false, - }, - 'experimental.delays': { - 'type': 'object', - 'additionalProperties': false, - 'properties': { - 'general': { - 'type': 'object', - 'description': 'Delays for all signals besides error and warning at position', - 'additionalProperties': false, - 'properties': { - 'announcement': { - 'description': localize('accessibility.signalOptions.delays.general.announcement', "The delay in milliseconds before an announcement is made."), - 'type': 'number', - 'minimum': 0, - 'default': 3000 - }, - 'sound': { - 'description': localize('accessibility.signalOptions.delays.general.sound', "The delay in milliseconds before a sound is played."), - 'type': 'number', - 'minimum': 0, - 'default': 400 - } - }, - }, - 'warningAtPosition': { - 'type': 'object', - 'additionalProperties': false, - 'properties': { - 'announcement': { - 'description': localize('accessibility.signalOptions.delays.warningAtPosition.announcement', "The delay in milliseconds before an announcement is made when there's a warning at the position."), - 'type': 'number', - 'minimum': 0, - 'default': 3000 - }, - 'sound': { - 'description': localize('accessibility.signalOptions.delays.warningAtPosition.sound', "The delay in milliseconds before a sound is played when there's a warning at the position."), - 'type': 'number', - 'minimum': 0, - 'default': 1000 - } - }, - }, - 'errorAtPosition': { - 'type': 'object', - 'additionalProperties': false, - 'properties': { - 'announcement': { - 'description': localize('accessibility.signalOptions.delays.errorAtPosition.announcement', "The delay in milliseconds before an announcement is made when there's an error at the position."), - 'type': 'number', - 'minimum': 0, - 'default': 3000 - }, - 'sound': { - 'description': localize('accessibility.signalOptions.delays.errorAtPosition.sound', "The delay in milliseconds before a sound is played when there's an error at the position."), - 'type': 'number', - 'minimum': 0, - 'default': 1000 - } - }, - }, - }, - 'default': { - 'general': { - 'announcement': 3000, - 'sound': 400 - }, - 'warningAtPosition': { - 'announcement': 3000, - 'sound': 1000 - }, - 'errorAtPosition': { - 'announcement': 3000, - 'sound': 1000 - } - } + 'sound': { + 'description': localize('accessibility.signalOptions.delays.general.sound', "The delay in milliseconds before a sound is played."), + 'type': 'number', + 'minimum': 0, + 'default': 400 + } + }, + 'tags': ['accessibility'] + }, + 'accessibility.signalOptions.experimental.delays.warningAtPosition': { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'announcement': { + 'description': localize('accessibility.signalOptions.delays.warningAtPosition.announcement', "The delay in milliseconds before an announcement is made when there's a warning at the position."), + 'type': 'number', + 'minimum': 0, + 'default': 3000 }, + 'sound': { + 'description': localize('accessibility.signalOptions.delays.warningAtPosition.sound', "The delay in milliseconds before a sound is played when there's a warning at the position."), + 'type': 'number', + 'minimum': 0, + 'default': 1000 + } }, - 'default': { - 'volume': 70, - 'debouncePositionChanges': false, - 'experimental.delays': { - 'general': { - 'announcement': 3000, - 'sound': 400 - }, - 'warningAtPosition': { - 'announcement': 3000, - 'sound': 1000 - }, - 'errorAtPosition': { - 'announcement': 3000, - 'sound': 1000 - } + 'tags': ['accessibility'] + }, + 'accessibility.signalOptions.experimental.delays.errorAtPosition': { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'announcement': { + 'description': localize('accessibility.signalOptions.delays.errorAtPosition.announcement', "The delay in milliseconds before an announcement is made when there's an error at the position."), + 'type': 'number', + 'minimum': 0, + 'default': 3000 + }, + 'sound': { + 'description': localize('accessibility.signalOptions.delays.errorAtPosition.sound', "The delay in milliseconds before a sound is played when there's an error at the position."), + 'type': 'number', + 'minimum': 0, + 'default': 1000 } }, - tags: ['accessibility'] + 'tags': ['accessibility'] }, 'accessibility.signals.lineHasBreakpoint': { ...signalFeatureBase, @@ -698,6 +662,11 @@ const configuration: IConfigurationNode = { 'announcement': 'never' } }, + 'accessibility.underlineLinks': { + 'type': 'boolean', + 'description': localize('accessibility.underlineLinks', "Controls whether links should be underlined in the workbench."), + 'default': false, + }, } }; @@ -804,10 +773,9 @@ export class DynamicSpeechAccessibilityConfiguration extends Disposable implemen Registry.as(WorkbenchExtensions.ConfigurationMigration) .registerConfigurationMigrations([{ key: 'audioCues.volume', - migrateFn: (volume, accessor) => { - const debouncePositionChanges = getDebouncePositionChangesFromConfig(accessor); + migrateFn: (value, accessor) => { return [ - ['accessibility.signalOptions', { value: debouncePositionChanges !== undefined ? { volume, debouncePositionChanges } : { volume } }], + ['accessibility.signalOptions.volume', { value }], ['audioCues.volume', { value: undefined }] ]; } @@ -816,10 +784,9 @@ Registry.as(WorkbenchExtensions.ConfigurationMi Registry.as(WorkbenchExtensions.ConfigurationMigration) .registerConfigurationMigrations([{ key: 'audioCues.debouncePositionChanges', - migrateFn: (debouncePositionChanges, accessor) => { - const volume = getVolumeFromConfig(accessor); + migrateFn: (value) => { return [ - ['accessibility.signalOptions', { value: volume !== undefined ? { volume, debouncePositionChanges } : { debouncePositionChanges } }], + ['accessibility.signalOptions.debouncePositionChanges', { value }], ['audioCues.debouncePositionChanges', { value: undefined }] ]; } @@ -829,12 +796,18 @@ Registry.as(WorkbenchExtensions.ConfigurationMi .registerConfigurationMigrations([{ key: 'accessibility.signalOptions', migrateFn: (value, accessor) => { - const delays = value.delays; - if (!delays) { - return []; - } + const delayGeneral = getDelaysFromConfig(accessor, 'general'); + const delayError = getDelaysFromConfig(accessor, 'errorAtPosition'); + const delayWarning = getDelaysFromConfig(accessor, 'warningAtPosition'); + const volume = getVolumeFromConfig(accessor); + const debouncePositionChanges = getDebouncePositionChangesFromConfig(accessor); return [ - ['accessibility.signalOptions', { value: { ...value, 'experimental.delays': delays, 'delays': undefined } }], + ['accessibility.signalOptions.volume', { value: volume }], + ['accessibility.signalOptions.debouncePositionChanges', { value: debouncePositionChanges }], + ['accessibility.signalOptions.experimental.delays.general', { value: delayGeneral }], + ['accessibility.signalOptions.experimental.delays.errorAtPosition', { value: delayError }], + ['accessibility.signalOptions.experimental.delays.warningAtPosition', { value: delayWarning }], + ['accessibility.signalOptions', { value: undefined }], ]; } }]); @@ -843,10 +816,9 @@ Registry.as(WorkbenchExtensions.ConfigurationMi Registry.as(WorkbenchExtensions.ConfigurationMigration) .registerConfigurationMigrations([{ key: 'accessibility.signals.sounds.volume', - migrateFn: (volume, accessor) => { - const debouncePositionChanges = getDebouncePositionChangesFromConfig(accessor); + migrateFn: (value) => { return [ - ['accessibility.signalOptions', { value: debouncePositionChanges !== undefined ? { volume, debouncePositionChanges } : { volume } }], + ['accessibility.signalOptions.volume', { value }], ['accessibility.signals.sounds.volume', { value: undefined }] ]; } @@ -855,21 +827,24 @@ Registry.as(WorkbenchExtensions.ConfigurationMi Registry.as(WorkbenchExtensions.ConfigurationMigration) .registerConfigurationMigrations([{ key: 'accessibility.signals.debouncePositionChanges', - migrateFn: (debouncePositionChanges, accessor) => { - const volume = getVolumeFromConfig(accessor); + migrateFn: (value) => { return [ - ['accessibility.signalOptions', { value: volume !== undefined ? { volume, debouncePositionChanges } : { debouncePositionChanges } }], + ['accessibility.signalOptions.debouncePositionChanges', { value }], ['accessibility.signals.debouncePositionChanges', { value: undefined }] ]; } }]); +function getDelaysFromConfig(accessor: (key: string) => any, type: 'general' | 'errorAtPosition' | 'warningAtPosition'): { announcement: number; sound: number } | undefined { + return accessor(`accessibility.signalOptions.experimental.delays.${type}`) || accessor('accessibility.signalOptions')?.['experimental.delays']?.[`${type}`] || accessor('accessibility.signalOptions')?.['delays']?.[`${type}`]; +} + function getVolumeFromConfig(accessor: (key: string) => any): string | undefined { - return accessor('accessibility.signalOptions')?.volume || accessor('accessibility.signals.sounds.volume') || accessor('audioCues.volume'); + return accessor('accessibility.signalOptions.volume') || accessor('accessibility.signalOptions')?.volume || accessor('accessibility.signals.sounds.volume') || accessor('audioCues.volume'); } function getDebouncePositionChangesFromConfig(accessor: (key: string) => any): number | undefined { - return accessor('accessibility.signalOptions')?.debouncePositionChanges || accessor('accessibility.signals.debouncePositionChanges') || accessor('audioCues.debouncePositionChanges'); + return accessor('accessibility.signalOptions.debouncePositionChanges') || accessor('accessibility.signalOptions')?.debouncePositionChanges || accessor('accessibility.signals.debouncePositionChanges') || accessor('audioCues.debouncePositionChanges'); } Registry.as(WorkbenchExtensions.ConfigurationMigration) diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution.ts index 336832eb16c..f0bc218c0c1 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution.ts @@ -19,7 +19,7 @@ export class AccessibilitySignalLineDebuggerContribution ) { super(); - const isEnabled = observableFromEvent( + const isEnabled = observableFromEvent(this, accessibilitySignalService.onSoundEnabledChanged(AccessibilitySignal.onDebugBreak), () => accessibilitySignalService.isSoundEnabled(AccessibilitySignal.onDebugBreak) ); diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts index de1c2f798af..4fa9f2d85c9 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts @@ -35,7 +35,7 @@ export class EditorTextPropertySignalsContribution extends Disposable implements .some(signal => observableFromValueWithChangeEvent(this, this._accessibilitySignalService.getEnabledState(signal, false)).read(reader)) ); - private readonly _activeEditorObservable = observableFromEvent( + private readonly _activeEditorObservable = observableFromEvent(this, this._editorService.onDidActiveEditorChange, (_) => { const activeTextEditorControl = this._editorService.activeTextEditorControl; @@ -104,7 +104,7 @@ export class EditorTextPropertySignalsContribution extends Disposable implements for (const modality of ['sound', 'announcement'] as AccessibilityModality[]) { if (this._accessibilitySignalService.getEnabledState(signal, false, modality).value) { - const delay = this._accessibilitySignalService.getDelayMs(signal, modality) + (didType.get() ? 1000 : 0); + const delay = this._accessibilitySignalService.getDelayMs(signal, modality, mode) + (didType.get() ? 1000 : 0); timeouts.add(disposableTimeout(() => { if (source.isPresent(position, mode, undefined)) { diff --git a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts index 22c507ca0b3..dc00b4d61d2 100644 --- a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts +++ b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { mockObject } from 'vs/base/test/common/mock'; diff --git a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts index a05e30502cc..50cd0b41f8c 100644 --- a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts +++ b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { IFileService } from 'vs/platform/files/common/files'; import { mock } from 'vs/workbench/test/common/workbenchTestServices'; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index bf426bec1b8..b872b925229 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -15,13 +15,12 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IsLinuxContext, IsWindowsContext } from 'vs/platform/contextkey/common/contextkeys'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputButton, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { CHAT_VIEW_ID, IChatWidgetService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatDetail, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; @@ -102,10 +101,9 @@ class OpenChatGlobalAction extends Action2 { } } -class ChatHistoryAction extends ViewAction { +class ChatHistoryAction extends Action2 { constructor() { super({ - viewId: CHAT_VIEW_ID, id: `workbench.action.chat.history`, title: localize2('chat.history.label', "Show Chats..."), menu: { @@ -121,12 +119,11 @@ class ChatHistoryAction extends ViewAction { }); } - async runInView(accessor: ServicesAccessor, view: ChatViewPane) { + async run(accessor: ServicesAccessor) { const chatService = accessor.get(IChatService); const quickInputService = accessor.get(IQuickInputService); const viewsService = accessor.get(IViewsService); const editorService = accessor.get(IEditorService); - const items = chatService.getHistory(); const openInEditorButton: IQuickInputButton = { iconClass: ThemeIcon.asClassName(Codicon.file), @@ -140,25 +137,30 @@ class ChatHistoryAction extends ViewAction { interface IChatPickerItem extends IQuickPickItem { chat: IChatDetail; } - const picks: IChatPickerItem[] = items.map((i): IChatPickerItem => ({ - label: i.title, - chat: i, - buttons: [ - openInEditorButton, - deleteButton - ] - })); + + const getPicks = () => { + const items = chatService.getHistory(); + return items.map((i): IChatPickerItem => ({ + label: i.title, + chat: i, + buttons: [ + openInEditorButton, + deleteButton + ] + })); + }; + const store = new DisposableStore(); const picker = store.add(quickInputService.createQuickPick()); picker.placeholder = localize('interactiveSession.history.pick', "Switch to chat"); - picker.items = picks; + picker.items = getPicks(); store.add(picker.onDidTriggerItemButton(context => { if (context.button === openInEditorButton) { editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { target: { sessionId: context.item.chat.sessionId }, pinned: true } }, ACTIVE_GROUP); picker.hide(); } else if (context.button === deleteButton) { chatService.removeHistoryEntry(context.item.chat.sessionId); - picker.items = picks.filter(i => i !== context.item); + picker.items = getPicks(); } })); store.add(picker.onDidAccept(async () => { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 6d9b8c58a43..b94a9434cc5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -17,12 +17,14 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { CopyAction } from 'vs/editor/contrib/clipboard/browser/clipboard'; -import { localize2 } from 'vs/nls'; +import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { accessibleViewInCodeBlock } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; @@ -182,7 +184,7 @@ export function registerChatCodeBlockActions() { constructor() { super({ id: 'workbench.action.chat.insertCodeBlock', - title: localize2('interactive.insertCodeBlock.label', "Insert at Cursor"), + title: localize2('interactive.insertCodeBlock.label', "Apply in Editor"), precondition: CONTEXT_CHAT_ENABLED, f1: true, category: CHAT_CATEGORY, @@ -267,6 +269,8 @@ export function registerChatCodeBlockActions() { const bulkEditService = accessor.get(IBulkEditService); const codeEditorService = accessor.get(ICodeEditorService); + const progressService = accessor.get(IProgressService); + const notificationService = accessor.get(INotificationService); const mappedEditsProviders = accessor.get(ILanguageFeaturesService).mappedEditsProvider.ordered(activeModel); @@ -275,7 +279,6 @@ export function registerChatCodeBlockActions() { let mappedEdits: WorkspaceEdit | null = null; if (mappedEditsProviders.length > 0) { - const mostRelevantProvider = mappedEditsProviders[0]; // TODO@ulugbekna: should we try all providers? // 0th sub-array - editor selections array if there are any selections // 1st sub-array - array with documents used to get the chat reply @@ -304,14 +307,37 @@ export function registerChatCodeBlockActions() { const cancellationTokenSource = new CancellationTokenSource(); - mappedEdits = await mostRelevantProvider.provideMappedEdits( - activeModel, - [codeBlockActionContext.code], - { documents: docRefs }, - cancellationTokenSource.token); + try { + mappedEdits = await progressService.withProgress( + { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, + async progress => { + progress.report({ message: localize('applyCodeBlock.progress', "Applying code block...") }); + + for (const provider of mappedEditsProviders) { + const mappedEdits = await provider.provideMappedEdits( + activeModel, + [codeBlockActionContext.code], + { documents: docRefs }, + cancellationTokenSource.token + ); + if (mappedEdits) { + return mappedEdits; + } + } + return null; + }, + () => cancellationTokenSource.cancel() + ); + } catch (e) { + notificationService.notify({ severity: Severity.Error, message: localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message) }); + } finally { + cancellationTokenSource.dispose(); + } + } if (mappedEdits) { + console.log('Mapped edits:', mappedEdits); await bulkEditService.apply(mappedEdits); } else { const activeSelection = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1); @@ -497,7 +523,7 @@ export function registerChatCodeBlockActions() { const currentResponse = curCodeBlockInfo ? curCodeBlockInfo.element : (focusedResponse ?? widget.viewModel?.getItems().reverse().find((item): item is IChatResponseViewModel => isResponseVM(item))); - if (!currentResponse) { + if (!currentResponse || !isResponseVM(currentResponse)) { return; } @@ -610,7 +636,8 @@ export function registerChatCodeCompareBlockActions() { precondition: ContextKeyExpr.and(EditorContextKeys.hasChanges, CONTEXT_CHAT_EDIT_APPLIED.negate()), menu: { id: MenuId.ChatCompareBlock, - group: 'navigation' + group: 'navigation', + order: 1, } }); } @@ -629,4 +656,28 @@ export function registerChatCodeCompareBlockActions() { }); } }); + + registerAction2(class DiscardEditsCompareBlockAction extends ChatCompareCodeBlockAction { + constructor() { + super({ + id: 'workbench.action.chat.discardCompareEdits', + title: localize2('interactive.compare.discard', "Discard Edits"), + f1: false, + category: CHAT_CATEGORY, + icon: Codicon.trash, + precondition: ContextKeyExpr.and(EditorContextKeys.hasChanges, CONTEXT_CHAT_EDIT_APPLIED.negate()), + menu: { + id: MenuId.ChatCompareBlock, + group: 'navigation', + order: 2, + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise { + const instaService = accessor.get(IInstantiationService); + const editor = instaService.createInstance(DefaultChatTextEditor); + editor.discard(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 01a8f050bac..ee4b0beba48 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -30,9 +30,14 @@ import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVari import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorType } from 'vs/editor/common/editorCommon'; +import { compare } from 'vs/base/common/strings'; export function registerChatContextActions() { registerAction2(AttachContextAction); + registerAction2(AttachFileAction); + registerAction2(AttachSelectionAction); } export type IChatContextQuickPickItem = IFileQuickPickItem | IDynamicVariableQuickPickItem | IStaticVariableQuickPickItem | IGotoSymbolQuickPickItem | ISymbolQuickPickItem | IQuickAccessQuickPickItem; @@ -77,6 +82,58 @@ export interface IQuickAccessQuickPickItem extends IQuickPickItem { prefix: string; } +class AttachFileAction extends Action2 { + + static readonly ID = 'workbench.action.chat.attachFile'; + + constructor() { + super({ + id: AttachFileAction.ID, + title: localize2('workbench.action.chat.attachFile.label', "Attach File"), + category: CHAT_CATEGORY, + f1: false + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const variablesService = accessor.get(IChatVariablesService); + const textEditorService = accessor.get(IEditorService); + + const activeUri = textEditorService.activeEditor?.resource; + if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote].includes(activeUri.scheme)) { + variablesService.attachContext('file', activeUri, ChatAgentLocation.Panel); + } + } +} + +class AttachSelectionAction extends Action2 { + + static readonly ID = 'workbench.action.chat.attachSelection'; + + constructor() { + super({ + id: AttachSelectionAction.ID, + title: localize2('workbench.action.chat.attachSelection.label', "Add Selection to Chat"), + category: CHAT_CATEGORY, + f1: false + }); + } + + 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].includes(activeUri.scheme)) { + const selection = activeEditor?.getSelection(); + if (selection) { + variablesService.attachContext('file', { uri: activeUri, range: selection }, ChatAgentLocation.Panel); + } + } + } +} + class AttachContextAction extends Action2 { static readonly ID = 'workbench.action.chat.attachContext'; @@ -231,7 +288,21 @@ class AttachContextAction extends Action2 { prefix: SymbolsQuickAccessProvider.PREFIX }); - this._show(quickInputService, commandService, widget, quickPickItems); + function extractTextFromIconLabel(label: string | undefined): string { + if (!label) { + return ''; + } + const match = label.match(/\$\([^\)]+\)\s*(.+)/); + return match ? match[1] : label; + } + + this._show(quickInputService, commandService, widget, quickPickItems.sort(function (a, b) { + + const first = extractTextFromIconLabel(a.label).toUpperCase(); + const second = extractTextFromIconLabel(b.label).toUpperCase(); + + return compare(first, second); + })); } private _show(quickInputService: IQuickInputService, commandService: ICommandService, widget: IChatWidget, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[], query: string = '') { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts new file mode 100644 index 00000000000..4ab735e2b4e --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.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 { Codicon } from 'vs/base/common/codicons'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { localize2 } from 'vs/nls'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; + +export function registerChatDeveloperActions() { + registerAction2(LogChatInputHistoryAction); +} + +class LogChatInputHistoryAction extends Action2 { + + static readonly ID = 'workbench.action.chat.logInputHistory'; + + constructor() { + super({ + id: LogChatInputHistoryAction.ID, + title: localize2('workbench.action.chat.logInputHistory.label', "Log Chat Input History"), + icon: Codicon.attach, + category: Categories.Developer, + f1: true + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const chatWidgetService = accessor.get(IChatWidgetService); + chatWidgetService.lastFocusedWidget?.logInputHistory(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 5a6574db112..b7d76a5a227 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -161,6 +161,7 @@ export class CancelAction extends Action2 { keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.Escape, + win: { primary: KeyMod.Alt | KeyCode.Backspace }, } }); } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 7d45fc86091..4937c6afbfd 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -43,10 +43,12 @@ import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/b import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; import 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; import 'vs/workbench/contrib/chat/browser/contrib/chatInputCompletions'; +import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover'; import { ChatAgentLocation, ChatAgentNameService, ChatAgentService, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; +import { LanguageModelToolsService, ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; @@ -57,6 +59,8 @@ import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/s import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; import { registerChatContextActions } from 'vs/workbench/contrib/chat/browser/actions/chatContextActions'; +import { registerChatDeveloperActions } from 'vs/workbench/contrib/chat/browser/actions/chatDeveloperActions'; +import { LanguageModelToolsExtensionPointHandler } from 'vs/workbench/contrib/chat/common/tools/languageModelToolsContribution'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -97,10 +101,23 @@ configurationRegistry.registerConfiguration({ deprecated: true, default: false }, + 'chat.experimental.variables.editor': { + type: 'boolean', + description: nls.localize('chat.experimental.variables.editor', "Enables variables for editor chat."), + default: false + }, + 'chat.experimental.variables.notebook': { + type: 'boolean', + description: nls.localize('chat.experimental.variables.notebook', "Enables variables for notebook chat."), + default: false + }, + 'chat.experimental.variables.terminal': { + type: 'boolean', + description: nls.localize('chat.experimental.variables.terminal', "Enables variables for terminal chat."), + default: false + }, } }); - - Registry.as(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( ChatEditor, @@ -236,6 +253,7 @@ registerWorkbenchContribution2(ChatResolverContribution.ID, ChatResolverContribu workbenchContributionsRegistry.registerWorkbenchContribution(ChatSlashStaticSlashCommandsContribution, LifecyclePhase.Eventually); Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer); registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); +registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.Eventually); registerChatActions(); registerChatCopyActions(); @@ -249,6 +267,7 @@ registerChatExportActions(); registerMoveActions(); registerNewChatActions(); registerChatContextActions(); +registerChatDeveloperActions(); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed); @@ -261,5 +280,6 @@ registerSingleton(IChatSlashCommandService, ChatSlashCommandService, Instantiati registerSingleton(IChatAgentService, ChatAgentService, InstantiationType.Delayed); registerSingleton(IChatAgentNameService, ChatAgentNameService, InstantiationType.Delayed); registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType.Delayed); +registerSingleton(ILanguageModelToolsService, LanguageModelToolsService, InstantiationType.Delayed); registerSingleton(IVoiceChatService, VoiceChatService, InstantiationType.Delayed); registerSingleton(IChatCodeBlockContextProviderService, ChatCodeBlockContextProviderService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 48844b393d8..c02338c87be 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -79,7 +79,8 @@ export interface IChatAccessibilityService { export interface IChatCodeBlockInfo { codeBlockIndex: number; - element: IChatResponseViewModel; + element: ChatTreeItem; + uri: URI | undefined; focus(): void; } @@ -92,7 +93,7 @@ export interface IChatFileTreeInfo { export type ChatTreeItem = IChatRequestViewModel | IChatResponseViewModel | IChatWelcomeMessageViewModel; export interface IChatListItemRendererOptions { - readonly renderStyle?: 'default' | 'compact'; + readonly renderStyle?: 'default' | 'compact' | 'minimal'; readonly noHeader?: boolean; readonly noPadding?: boolean; readonly editableCodeBlock?: boolean; @@ -102,13 +103,22 @@ export interface IChatListItemRendererOptions { export interface IChatWidgetViewOptions { renderInputOnTop?: boolean; renderFollowups?: boolean; - renderStyle?: 'default' | 'compact'; + renderStyle?: 'default' | 'compact' | 'minimal'; supportsFileReferences?: boolean; filter?: (item: ChatTreeItem) => boolean; rendererOptions?: IChatListItemRendererOptions; menus?: { + /** + * The menu that is inside the input editor, use for send, dictation + */ executeToolbar?: MenuId; + /** + * The menu that next to the input editor, use for close, config etc + */ inputSideToolbar?: MenuId; + /** + * The telemetry source for all commands of this widget + */ telemetrySource?: string; }; defaultElementHeight?: number; @@ -148,6 +158,7 @@ export interface IChatWidget { getFocus(): ChatTreeItem | undefined; setInput(query?: string): void; getInput(): string; + logInputHistory(): void; acceptInput(query?: string): Promise; acceptInputWithPrefix(prefix: string): void; setInputPlaceholder(placeholder: string): void; diff --git a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts index 9163fd24fdb..3b1a011b5c5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -5,10 +5,11 @@ import * as dom from 'vs/base/browser/dom'; import { h } from 'vs/base/browser/dom'; -import { IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover'; +import { IManagedHoverOptions } from 'vs/base/browser/ui/hover/hover'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -29,6 +30,9 @@ export class ChatAgentHover extends Disposable { private readonly publisherName: HTMLElement; private readonly description: HTMLElement; + private readonly _onDidChangeContents = this._register(new Emitter()); + public readonly onDidChangeContents: Event = this._onDidChangeContents.event; + constructor( @IChatAgentService private readonly chatAgentService: IChatAgentService, @IExtensionsWorkbenchService private readonly extensionService: IExtensionsWorkbenchService, @@ -110,13 +114,14 @@ export class ChatAgentHover extends Disposable { const extension = extensions[0]; if (extension?.publisherDomain?.verified) { this.domNode.classList.toggle('verifiedPublisher', true); + this._onDidChangeContents.fire(); } }); } } } -export function getChatAgentHoverOptions(getAgent: () => IChatAgentData | undefined, commandService: ICommandService): IUpdatableHoverOptions { +export function getChatAgentHoverOptions(getAgent: () => IChatAgentData | undefined, commandService: ICommandService): IManagedHoverOptions { return { actions: [ { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollections.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollections.ts new file mode 100644 index 00000000000..d13bbdff23b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollections.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; + +export class ResourcePool extends Disposable { + private readonly pool: T[] = []; + + private _inUse = new Set; + public get inUse(): ReadonlySet { + return this._inUse; + } + + constructor( + private readonly _itemFactory: () => T, + ) { + super(); + } + + get(): T { + if (this.pool.length > 0) { + const item = this.pool.pop()!; + this._inUse.add(item); + return item; + } + + const item = this._register(this._itemFactory()); + this._inUse.add(item); + return item; + } + + release(item: T): void { + this._inUse.delete(item); + this.pool.push(item); + } +} + +export interface IDisposableReference extends IDisposable { + object: T; + isStale: () => boolean; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart.ts new file mode 100644 index 00000000000..3893117fd20 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatCommandButton } from 'vs/workbench/contrib/chat/common/chatService'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +const $ = dom.$; + +export class ChatCommandButtonContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + constructor( + commandButton: IChatCommandButton, + context: IChatContentPartRenderContext, + @ICommandService private readonly commandService: ICommandService + ) { + super(); + + this.domNode = $('.chat-command-button'); + const enabled = !isResponseVM(context.element) || !context.element.isStale; + const tooltip = enabled ? + commandButton.command.tooltip : + localize('commandButtonDisabled', "Button not available in restored chat"); + const button = this._register(new Button(this.domNode, { ...defaultButtonStyles, supportIcons: true, title: tooltip })); + button.label = commandButton.command.title; + button.enabled = enabled; + + // TODO still need telemetry for command buttons + this._register(button.onDidClick(() => this.commandService.executeCommand(commandButton.command.id, ...(commandButton.command.arguments ?? [])))); + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'command'; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts new file mode 100644 index 00000000000..4159f07c919 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ChatConfirmationWidget } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatConfirmation, IChatSendRequestOptions, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export class ChatConfirmationContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + constructor( + confirmation: IChatConfirmation, + context: IChatContentPartRenderContext, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IChatService private readonly chatService: IChatService, + ) { + super(); + + const element = context.element; + const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, confirmation.message, [ + { label: localize('accept', "Accept"), data: confirmation.data }, + { label: localize('dismiss', "Dismiss"), data: confirmation.data, isSecondary: true }, + ])); + confirmationWidget.setShowButtons(!confirmation.isUsed); + + this._register(confirmationWidget.onDidClick(async e => { + if (isResponseVM(element)) { + const prompt = `${e.label}: "${confirmation.title}"`; + const data: IChatSendRequestOptions = e.isSecondary ? + { rejectedConfirmationData: [e.data] } : + { acceptedConfirmationData: [e.data] }; + data.agentId = element.agent?.id; + data.slashCommand = element.slashCommand?.name; + if (await this.chatService.sendRequest(element.sessionId, prompt, data)) { + confirmation.isUsed = true; + confirmationWidget.setShowButtons(false); + this._onDidChangeHeight.fire(); + } + } + })); + + this.domNode = confirmationWidget.domNode; + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'confirmation'; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts similarity index 100% rename from src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts rename to src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts.ts new file mode 100644 index 00000000000..80e3c44a056 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatRendererContent } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export interface IChatContentPart extends IDisposable { + domNode: HTMLElement; + + /** + * Returns true if the other content is equivalent to what is already rendered in this content part. + * Returns false if a rerender is needed. + * followingContent is all the content that will be rendered after this content part (to support progress messages' behavior). + */ + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean; +} + +export interface IChatContentPartRenderContext { + element: ChatTreeItem; + index: number; + content: ReadonlyArray; + preceedingContentParts: ReadonlyArray; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts new file mode 100644 index 00000000000..29a2573374b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Emitter } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { Range } from 'vs/editor/common/core/range'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatCodeBlockInfo, IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; +import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; +import { CodeBlockPart, ICodeBlockData, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { IMarkdownVulnerability } from 'vs/workbench/contrib/chat/common/annotations'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { CodeBlockModelCollection } from 'vs/workbench/contrib/chat/common/codeBlockModelCollection'; + +const $ = dom.$; + +export class ChatMarkdownContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + private readonly allRefs: IDisposableReference[] = []; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + public readonly codeblocks: IChatCodeBlockInfo[] = []; + + constructor( + private readonly markdown: IMarkdownString, + context: IChatContentPartRenderContext, + private readonly editorPool: EditorPool, + fillInIncompleteTokens = false, + codeBlockStartIndex = 0, + renderer: MarkdownRenderer, + currentWidth: number, + private readonly codeBlockModelCollection: CodeBlockModelCollection, + rendererOptions: IChatListItemRendererOptions, + @IContextKeyService contextKeyService: IContextKeyService, + @ITextModelService private readonly textModelService: ITextModelService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + const element = context.element; + const markdownDecorationsRenderer = instantiationService.createInstance(ChatMarkdownDecorationsRenderer); + + // We release editors in order so that it's more likely that the same editor will be assigned if this element is re-rendered right away, like it often is during progressive rendering + const orderedDisposablesList: IDisposable[] = []; + let codeBlockIndex = codeBlockStartIndex; + const result = this._register(renderer.render(markdown, { + fillInIncompleteTokens, + codeBlockRendererSync: (languageId, text) => { + const index = codeBlockIndex++; + let textModel: Promise; + let range: Range | undefined; + let vulns: readonly IMarkdownVulnerability[] | undefined; + if (equalsIgnoreCase(languageId, localFileLanguageId)) { + try { + const parsedBody = parseLocalFileData(text); + range = parsedBody.range && Range.lift(parsedBody.range); + textModel = this.textModelService.createModelReference(parsedBody.uri).then(ref => ref.object); + } catch (e) { + return $('div'); + } + } else { + if (!isRequestVM(element) && !isResponseVM(element)) { + console.error('Trying to render code block in welcome', element.id, index); + return $('div'); + } + + const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; + const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, index); + vulns = modelEntry.vulns; + textModel = modelEntry.model; + } + + const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; + const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns }, text, currentWidth, rendererOptions.editableCodeBlock); + this.allRefs.push(ref); + + // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) + // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) + this._register(ref.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire())); + + const info: IChatCodeBlockInfo = { + codeBlockIndex: index, + element, + focus() { + ref.object.focus(); + }, + uri: ref.object.uri + }; + this.codeblocks.push(info); + orderedDisposablesList.push(ref); + return ref.object.element; + }, + asyncRenderCallback: () => this._onDidChangeHeight.fire(), + })); + + this._register(markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(result.element)); + + orderedDisposablesList.reverse().forEach(d => this._register(d)); + this.domNode = result.element; + } + + private renderCodeBlock(data: ICodeBlockData, text: string, currentWidth: number, editableCodeBlock: boolean | undefined): IDisposableReference { + const ref = this.editorPool.get(); + const editorInfo = ref.object; + if (isResponseVM(data.element)) { + this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }); + } + + editorInfo.render(data, currentWidth, editableCodeBlock); + + return ref; + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + return other.kind === 'markdownContent' && other.content.value === this.markdown.value; + } + + layout(width: number): void { + this.allRefs.forEach(ref => ref.object.layout(width)); + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} + +export class EditorPool extends Disposable { + + private readonly _pool: ResourcePool; + + public inUse(): Iterable { + return this._pool.inUse; + } + + constructor( + options: ChatEditorOptions, + delegate: IChatRendererDelegate, + overflowWidgetsDomNode: HTMLElement | undefined, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + this._pool = this._register(new ResourcePool(() => { + return instantiationService.createInstance(CodeBlockPart, options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode); + })); + } + + get(): IDisposableReference { + const codeBlock = this._pool.get(); + let stale = false; + return { + object: codeBlock, + isStale: () => stale, + dispose: () => { + codeBlock.reset(); + stale = true; + this._pool.release(codeBlock); + } + }; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts new file mode 100644 index 00000000000..6545fdfe9ad --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $ } from 'vs/base/browser/dom'; +import { alert } from 'vs/base/browser/ui/aria/aria'; +import { Codicon } from 'vs/base/common/codicons'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressMessage, IChatTask } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatRendererContent, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export class ChatProgressContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private readonly showSpinner: boolean; + + constructor( + progress: IChatProgressMessage | IChatTask, + renderer: MarkdownRenderer, + context: IChatContentPartRenderContext, + forceShowSpinner?: boolean, + forceShowMessage?: boolean + ) { + super(); + + const followingContent = context.content.slice(context.index + 1); + this.showSpinner = forceShowSpinner ?? shouldShowSpinner(followingContent, context.element); + const hideMessage = forceShowMessage !== true && followingContent.some(part => part.kind !== 'progressMessage'); + if (hideMessage) { + // Placeholder, don't show the progress message + this.domNode = $(''); + return; + } + + if (this.showSpinner) { + // TODO@roblourens is this the right place for this? + // this step is in progress, communicate it to SR users + alert(progress.content.value); + } + const codicon = this.showSpinner ? ThemeIcon.modify(Codicon.loading, 'spin').id : Codicon.check.id; + const markdown = new MarkdownString(`$(${codicon}) ${progress.content.value}`, { + supportThemeIcons: true + }); + const result = this._register(renderer.render(markdown)); + result.element.classList.add('progress-step'); + + this.domNode = result.element; + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + // Needs rerender when spinner state changes + const showSpinner = shouldShowSpinner(followingContent, element); + return other.kind === 'progressMessage' && this.showSpinner === showSpinner; + } +} + +function shouldShowSpinner(followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return isResponseVM(element) && !element.isComplete && followingContent.length === 0; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts new file mode 100644 index 00000000000..b1305e8e160 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -0,0 +1,299 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { matchesSomeScheme, Schemas } from 'vs/base/common/network'; +import { basename } from 'vs/base/common/path'; +import { basenameOrAuthority } from 'vs/base/common/resources'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { FileKind } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; +import { ColorScheme } from 'vs/workbench/browser/web.api'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; +import { IChatContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatContentReference, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { IChatRendererContent, IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; + +const $ = dom.$; + +export class ChatReferencesContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + constructor( + private readonly data: ReadonlyArray, + labelOverride: string | undefined, + element: IChatResponseViewModel, + contentReferencesListPool: ContentReferencesListPool, + @IOpenerService openerService: IOpenerService, + ) { + super(); + + const referencesLabel = labelOverride ?? (data.length > 1 ? + localize('usedReferencesPlural', "Used {0} references", data.length) : + localize('usedReferencesSingular', "Used {0} reference", 1)); + const iconElement = $('.chat-used-context-icon'); + const icon = (element: IChatResponseViewModel) => element.usedReferencesExpanded ? Codicon.chevronDown : Codicon.chevronRight; + iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); + const buttonElement = $('.chat-used-context-label', undefined); + + const collapseButton = this._register(new Button(buttonElement, { + buttonBackground: undefined, + buttonBorder: undefined, + buttonForeground: undefined, + buttonHoverBackground: undefined, + buttonSecondaryBackground: undefined, + buttonSecondaryForeground: undefined, + buttonSecondaryHoverBackground: undefined, + buttonSeparator: undefined + })); + this.domNode = $('.chat-used-context', undefined, buttonElement); + collapseButton.label = referencesLabel; + collapseButton.element.prepend(iconElement); + this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); + this.domNode.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); + this._register(collapseButton.onDidClick(() => { + iconElement.classList.remove(...ThemeIcon.asClassNameArray(icon(element))); + element.usedReferencesExpanded = !element.usedReferencesExpanded; + iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); + this.domNode.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); + this._onDidChangeHeight.fire(); + this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); + })); + + const ref = this._register(contentReferencesListPool.get()); + const list = ref.object; + this.domNode.appendChild(list.getHTMLElement().parentElement!); + + this._register(list.onDidOpen((e) => { + if (e.element && 'reference' in e.element) { + const uriOrLocation = 'variableName' in e.element.reference ? e.element.reference.value : e.element.reference; + const uri = URI.isUri(uriOrLocation) ? uriOrLocation : + uriOrLocation?.uri; + if (uri) { + openerService.open( + uri, + { + fromUserGesture: true, + editorOptions: { + ...e.editorOptions, + ...{ + selection: uriOrLocation && 'range' in uriOrLocation ? uriOrLocation.range : undefined + } + } + }); + } + } + })); + this._register(list.onContextMenu((e) => { + e.browserEvent.preventDefault(); + e.browserEvent.stopPropagation(); + })); + + const maxItemsShown = 6; + const itemsShown = Math.min(data.length, maxItemsShown); + const height = itemsShown * 22; + list.layout(height); + list.getHTMLElement().style.height = `${height}px`; + list.splice(0, list.length, data); + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return other.kind === 'references' && other.references.length === this.data.length; + } + + private updateAriaLabel(element: HTMLElement, label: string, expanded?: boolean): void { + element.ariaLabel = expanded ? localize('usedReferencesExpanded', "{0}, expanded", label) : localize('usedReferencesCollapsed', "{0}, collapsed", label); + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} + +export class ContentReferencesListPool extends Disposable { + private _pool: ResourcePool>; + + public get inUse(): ReadonlySet> { + return this._pool.inUse; + } + + constructor( + private _onDidChangeVisibility: Event, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IThemeService private readonly themeService: IThemeService, + ) { + super(); + this._pool = this._register(new ResourcePool(() => this.listFactory())); + } + + private listFactory(): WorkbenchList { + const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); + + const container = $('.chat-used-context-list'); + this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); + + const list = this.instantiationService.createInstance( + WorkbenchList, + 'ChatListRenderer', + container, + new ContentReferencesListDelegate(), + [this.instantiationService.createInstance(ContentReferencesListRenderer, resourceLabels)], + { + alwaysConsumeMouseWheel: false, + accessibilityProvider: { + getAriaLabel: (element: IChatContentReference | IChatWarningMessage) => { + if (element.kind === 'warning') { + return element.content.value; + } + const reference = element.reference; + if ('variableName' in reference) { + return reference.variableName; + } else if (URI.isUri(reference)) { + return basename(reference.path); + } else { + return basename(reference.uri.path); + } + }, + + getWidgetAriaLabel: () => localize('usedReferences', "Used References") + }, + dnd: { + getDragURI: (element: IChatContentReference | IChatWarningMessage) => { + if (element.kind === 'warning') { + return null; + } + const { reference } = element; + if ('variableName' in reference) { + return null; + } else if (URI.isUri(reference)) { + return reference.toString(); + } else { + return reference.uri.toString(); + } + }, + dispose: () => { }, + onDragOver: () => false, + drop: () => { }, + }, + }); + + return list; + } + + get(): IDisposableReference> { + const object = this._pool.get(); + let stale = false; + return { + object, + isStale: () => stale, + dispose: () => { + stale = true; + this._pool.release(object); + } + }; + } +} + +class ContentReferencesListDelegate implements IListVirtualDelegate { + getHeight(element: IChatContentReference): number { + return 22; + } + + getTemplateId(element: IChatContentReference): string { + return ContentReferencesListRenderer.TEMPLATE_ID; + } +} + +interface IChatContentReferenceListTemplate { + label: IResourceLabel; + templateDisposables: IDisposable; +} + +class ContentReferencesListRenderer implements IListRenderer { + static TEMPLATE_ID = 'contentReferencesListRenderer'; + readonly templateId: string = ContentReferencesListRenderer.TEMPLATE_ID; + + constructor( + private labels: ResourceLabels, + @IThemeService private readonly themeService: IThemeService, + @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, + ) { } + + renderTemplate(container: HTMLElement): IChatContentReferenceListTemplate { + const templateDisposables = new DisposableStore(); + const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); + return { templateDisposables, label }; + } + + + private getReferenceIcon(data: IChatContentReference): URI | ThemeIcon | undefined { + if (ThemeIcon.isThemeIcon(data.iconPath)) { + return data.iconPath; + } else { + return this.themeService.getColorTheme().type === ColorScheme.DARK && data.iconPath?.dark + ? data.iconPath?.dark + : data.iconPath?.light; + } + } + + renderElement(data: IChatContentReference | IChatWarningMessage, index: number, templateData: IChatContentReferenceListTemplate, height: number | undefined): void { + if (data.kind === 'warning') { + templateData.label.setResource({ name: data.content.value }, { icon: Codicon.warning }); + return; + } + + const reference = data.reference; + const icon = this.getReferenceIcon(data); + templateData.label.element.style.display = 'flex'; + if ('variableName' in reference) { + if (reference.value) { + const uri = URI.isUri(reference.value) ? reference.value : reference.value.uri; + templateData.label.setResource( + { + resource: uri, + name: basenameOrAuthority(uri), + description: `#${reference.variableName}`, + range: 'range' in reference.value ? reference.value.range : undefined, + }, { icon }); + } else { + const variable = this.chatVariablesService.getVariable(reference.variableName); + templateData.label.setLabel(`#${reference.variableName}`, undefined, { title: variable?.description }); + } + } else { + const uri = 'uri' in reference ? reference.uri : reference; + if (matchesSomeScheme(uri, Schemas.mailto, Schemas.http, Schemas.https)) { + templateData.label.setResource({ resource: uri, name: uri.toString() }, { icon: icon ?? Codicon.globe }); + } else { + templateData.label.setFile(uri, { + fileKind: FileKind.FILE, + // Should not have this live-updating data on a historical reference + fileDecorations: { badges: false, colors: false }, + range: 'range' in reference ? reference.range : undefined + }); + } + } + } + + disposeTemplate(templateData: IChatContentReferenceListTemplate): void { + templateData.templateDisposables.dispose(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts new file mode 100644 index 00000000000..a1a03e1cae1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { ChatProgressContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart'; +import { ChatReferencesContentPart, ContentReferencesListPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatTask } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export class ChatTaskContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + public readonly onDidChangeHeight: Event; + + constructor( + private readonly task: IChatTask, + contentReferencesListPool: ContentReferencesListPool, + renderer: MarkdownRenderer, + context: IChatContentPartRenderContext, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + if (task.progress.length) { + const refsPart = this._register(instantiationService.createInstance(ChatReferencesContentPart, task.progress, task.content.value, context.element as IChatResponseViewModel, contentReferencesListPool)); + this.domNode = dom.$('.chat-progress-task'); + this.domNode.appendChild(refsPart.domNode); + this.onDidChangeHeight = refsPart.onDidChangeHeight; + } else { + // #217645 + const isSettled = task.isSettled?.() ?? true; + const progressPart = this._register(instantiationService.createInstance(ChatProgressContentPart, task, renderer, context, !isSettled, true)); + this.domNode = progressPart.domNode; + this.onDidChangeHeight = Event.None; + } + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + return other.kind === 'progressTask' + && other.progress.length === this.task.progress.length + && other.isSettled() === this.task.isSettled(); + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart.ts new file mode 100644 index 00000000000..5f4e6556c5c --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart.ts @@ -0,0 +1,211 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { TextEdit } from 'vs/editor/common/languages'; +import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; +import { IModelService } from 'vs/editor/common/services/model'; +import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; +import { CodeCompareBlockPart, ICodeCompareBlockData, ICodeCompareBlockDiffData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +const $ = dom.$; + +export class ChatTextEditContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + private readonly ref: IDisposableReference | undefined; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + constructor( + chatTextEdit: IChatTextEditGroup, + context: IChatContentPartRenderContext, + rendererOptions: IChatListItemRendererOptions, + diffEditorPool: DiffEditorPool, + currentWidth: number, + @ITextModelService private readonly textModelService: ITextModelService, + @IModelService private readonly modelService: IModelService, + @IChatService private readonly chatService: IChatService, + ) { + super(); + const element = context.element; + + // TODO@jrieken move this into the CompareCodeBlock and properly say what kind of changes happen + if (rendererOptions.renderTextEditsAsSummary?.(chatTextEdit.uri)) { + if (isResponseVM(element) && element.response.value.every(item => item.kind === 'textEditGroup')) { + this.domNode = $('.interactive-edits-summary', undefined, !element.isComplete + ? localize('editsSummary1', "Making changes...") + : element.isCanceled + ? localize('edits0', "Making changes was aborted.") + : localize('editsSummary', "Made changes.")); + } else { + this.domNode = $('div'); + } + + // TODO@roblourens this case is now handled outside this Part in ChatListRenderer, but can it be cleaned up? + // return; + } else { + + + const cts = new CancellationTokenSource(); + + let isDisposed = false; + this._register(toDisposable(() => { + isDisposed = true; + cts.dispose(true); + })); + + this.ref = this._register(diffEditorPool.get()); + + // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) + // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) + this._register(this.ref.object.onDidChangeContentHeight(() => { + this._onDidChangeHeight.fire(); + })); + + const data: ICodeCompareBlockData = { + element, + edit: chatTextEdit, + diffData: (async () => { + + const ref = await this.textModelService.createModelReference(chatTextEdit.uri); + + if (isDisposed) { + ref.dispose(); + return; + } + + this._register(ref); + + const original = ref.object.textEditorModel; + let originalSha1: string = ''; + + if (chatTextEdit.state) { + originalSha1 = chatTextEdit.state.sha1; + } else { + const sha1 = new DefaultModelSHA1Computer(); + if (sha1.canComputeSHA1(original)) { + originalSha1 = sha1.computeSHA1(original); + chatTextEdit.state = { sha1: originalSha1, applied: 0 }; + } + } + + const modified = this.modelService.createModel( + createTextBufferFactoryFromSnapshot(original.createSnapshot()), + { languageId: original.getLanguageId(), onDidChange: Event.None }, + URI.from({ scheme: Schemas.vscodeChatCodeBlock, path: original.uri.path, query: generateUuid() }), + false + ); + const modRef = await this.textModelService.createModelReference(modified.uri); + this._register(modRef); + + const editGroups: ISingleEditOperation[][] = []; + if (isResponseVM(element)) { + const chatModel = this.chatService.getSession(element.sessionId)!; + + for (const request of chatModel.getRequests()) { + if (!request.response) { + continue; + } + for (const item of request.response.response.value) { + if (item.kind !== 'textEditGroup' || item.state?.applied || !isEqual(item.uri, chatTextEdit.uri)) { + continue; + } + for (const group of item.edits) { + const edits = group.map(TextEdit.asEditOperation); + editGroups.push(edits); + } + } + if (request.response === element.model) { + break; + } + } + } + + for (const edits of editGroups) { + modified.pushEditOperations(null, edits, () => null); + } + + return { + modified, + original, + originalSha1 + } satisfies ICodeCompareBlockDiffData; + })() + }; + this.ref.object.render(data, currentWidth, cts.token); + + this.domNode = this.ref.object.element; + } + } + + layout(width: number): void { + this.ref?.object.layout(width); + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'textEditGroup'; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} + +export class DiffEditorPool extends Disposable { + + private readonly _pool: ResourcePool; + + public inUse(): Iterable { + return this._pool.inUse; + } + + constructor( + options: ChatEditorOptions, + delegate: IChatRendererDelegate, + overflowWidgetsDomNode: HTMLElement | undefined, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + this._pool = this._register(new ResourcePool(() => { + return instantiationService.createInstance(CodeCompareBlockPart, options, MenuId.ChatCompareBlock, delegate, overflowWidgetsDomNode); + })); + } + + get(): IDisposableReference { + const codeBlock = this._pool.get(); + let stale = false; + return { + object: codeBlock, + isStale: () => stale, + dispose: () => { + codeBlock.reset(); + stale = true; + this._pool.release(codeBlock); + } + }; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart.ts new file mode 100644 index 00000000000..8742d2da602 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart.ts @@ -0,0 +1,225 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { IAsyncDataSource, ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { FileKind, FileType } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; +import { IChatContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService'; +import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; +import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; + +const $ = dom.$; + +export class ChatTreeContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + public readonly onDidFocus: Event; + + private tree: WorkbenchCompressibleAsyncDataTree; + + constructor( + data: IChatResponseProgressFileTreeData, + element: ChatTreeItem, + treePool: TreePool, + treeDataIndex: number, + @IOpenerService private readonly openerService: IOpenerService + ) { + super(); + + const ref = this._register(treePool.get()); + this.tree = ref.object; + this.onDidFocus = this.tree.onDidFocus; + + this._register(this.tree.onDidOpen((e) => { + if (e.element && !('children' in e.element)) { + this.openerService.open(e.element.uri); + } + })); + this._register(this.tree.onDidChangeCollapseState(() => { + this._onDidChangeHeight.fire(); + })); + this._register(this.tree.onContextMenu((e) => { + e.browserEvent.preventDefault(); + e.browserEvent.stopPropagation(); + })); + + this.tree.setInput(data).then(() => { + if (!ref.isStale()) { + this.tree.layout(); + this._onDidChangeHeight.fire(); + } + }); + + this.domNode = this.tree.getHTMLElement().parentElement!; + } + + domFocus() { + this.tree.domFocus(); + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'treeData'; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} + +export class TreePool extends Disposable { + private _pool: ResourcePool>; + + public get inUse(): ReadonlySet> { + return this._pool.inUse; + } + + constructor( + private _onDidChangeVisibility: Event, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IConfigurationService private readonly configService: IConfigurationService, + @IThemeService private readonly themeService: IThemeService, + ) { + super(); + this._pool = this._register(new ResourcePool(() => this.treeFactory())); + } + + private treeFactory(): WorkbenchCompressibleAsyncDataTree { + const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); + + const container = $('.interactive-response-progress-tree'); + this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); + + const tree = this.instantiationService.createInstance( + WorkbenchCompressibleAsyncDataTree, + 'ChatListRenderer', + container, + new ChatListTreeDelegate(), + new ChatListTreeCompressionDelegate(), + [new ChatListTreeRenderer(resourceLabels, this.configService.getValue('explorer.decorations'))], + new ChatListTreeDataSource(), + { + collapseByDefault: () => false, + expandOnlyOnTwistieClick: () => false, + identityProvider: { + getId: (e: IChatResponseProgressFileTreeData) => e.uri.toString() + }, + accessibilityProvider: { + getAriaLabel: (element: IChatResponseProgressFileTreeData) => element.label, + getWidgetAriaLabel: () => localize('treeAriaLabel', "File Tree") + }, + alwaysConsumeMouseWheel: false + }); + + return tree; + } + + get(): IDisposableReference> { + const object = this._pool.get(); + let stale = false; + return { + object, + isStale: () => stale, + dispose: () => { + stale = true; + this._pool.release(object); + } + }; + } +} + +class ChatListTreeDelegate implements IListVirtualDelegate { + static readonly ITEM_HEIGHT = 22; + + getHeight(element: IChatResponseProgressFileTreeData): number { + return ChatListTreeDelegate.ITEM_HEIGHT; + } + + getTemplateId(element: IChatResponseProgressFileTreeData): string { + return 'chatListTreeTemplate'; + } +} + +class ChatListTreeCompressionDelegate implements ITreeCompressionDelegate { + isIncompressible(element: IChatResponseProgressFileTreeData): boolean { + return !element.children; + } +} + +interface IChatListTreeRendererTemplate { + templateDisposables: DisposableStore; + label: IResourceLabel; +} + +class ChatListTreeRenderer implements ICompressibleTreeRenderer { + templateId: string = 'chatListTreeTemplate'; + + constructor(private labels: ResourceLabels, private decorations: IFilesConfiguration['explorer']['decorations']) { } + + renderCompressedElements(element: ITreeNode, void>, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { + templateData.label.element.style.display = 'flex'; + const label = element.element.elements.map((e) => e.label); + templateData.label.setResource({ resource: element.element.elements[0].uri, name: label }, { + title: element.element.elements[0].label, + fileKind: element.children ? FileKind.FOLDER : FileKind.FILE, + extraClasses: ['explorer-item'], + fileDecorations: this.decorations + }); + } + renderTemplate(container: HTMLElement): IChatListTreeRendererTemplate { + const templateDisposables = new DisposableStore(); + const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); + return { templateDisposables, label }; + } + renderElement(element: ITreeNode, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { + templateData.label.element.style.display = 'flex'; + if (!element.children.length && element.element.type !== FileType.Directory) { + templateData.label.setFile(element.element.uri, { + fileKind: FileKind.FILE, + hidePath: true, + fileDecorations: this.decorations, + }); + } else { + templateData.label.setResource({ resource: element.element.uri, name: element.element.label }, { + title: element.element.label, + fileKind: FileKind.FOLDER, + fileDecorations: this.decorations + }); + } + } + disposeTemplate(templateData: IChatListTreeRendererTemplate): void { + templateData.templateDisposables.dispose(); + } +} + +class ChatListTreeDataSource implements IAsyncDataSource { + hasChildren(element: IChatResponseProgressFileTreeData): boolean { + return !!element.children; + } + + async getChildren(element: IChatResponseProgressFileTreeData): Promise> { + return element.children ?? []; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatWarningContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatWarningContentPart.ts new file mode 100644 index 00000000000..3fd0b9fb239 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatWarningContentPart.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Codicon } from 'vs/base/common/codicons'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { IChatContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; + +const $ = dom.$; + +export class ChatWarningContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + constructor( + kind: 'info' | 'warning' | 'error', + content: IMarkdownString, + renderer: MarkdownRenderer, + ) { + super(); + + this.domNode = $('.chat-notification-widget'); + let icon; + let iconClass; + switch (kind) { + case 'warning': + icon = Codicon.warning; + iconClass = '.chat-warning-codicon'; + break; + case 'error': + icon = Codicon.error; + iconClass = '.chat-error-codicon'; + break; + case 'info': + icon = Codicon.info; + iconClass = '.chat-info-codicon'; + break; + } + this.domNode.appendChild($(iconClass, undefined, renderIcon(icon))); + const markdownContent = renderer.render(content); + this.domNode.appendChild(markdownContent.element); + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'warning'; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css similarity index 100% rename from src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css rename to src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 30e33ebabd2..abf61519b38 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -6,14 +6,16 @@ import * as dom from 'vs/base/browser/dom'; import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; import { IHistoryNavigationWidget } from 'vs/base/browser/history'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { Range } from 'vs/editor/common/core/range'; import { Button } from 'vs/base/browser/ui/button/button'; import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; -import { HistoryNavigator } from 'vs/base/common/history'; +import { HistoryNavigator2 } from 'vs/base/common/history'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { basename, dirname } from 'vs/base/common/path'; import { isMacintosh } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; @@ -21,6 +23,7 @@ import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IDimension } from 'vs/editor/common/core/dimension'; import { IPosition } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; @@ -38,6 +41,7 @@ import { registerAndCreateHistoryNavigationContext } from 'vs/platform/history/b import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ResourceLabels } from 'vs/workbench/browser/labels'; @@ -53,9 +57,6 @@ import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatHistoryEntry, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { basename, dirname } from 'vs/base/common/path'; const $ = dom.$; @@ -130,10 +131,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._inputEditor; } - private history: HistoryNavigator; + private history: HistoryNavigator2; private historyNavigationBackwardsEnablement!: IContextKey; private historyNavigationForewardsEnablement!: IContextKey; - private onHistoryEntry = false; private inHistoryNavigation = false; private inputModel: ITextModel | undefined; private inputEditorHasText: IContextKey; @@ -156,6 +156,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @IConfigurationService private readonly configurationService: IConfigurationService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @ILogService private readonly logService: ILogService, ) { super(); @@ -165,8 +166,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatCursorAtTop = CONTEXT_CHAT_INPUT_CURSOR_AT_TOP.bindTo(contextKeyService); this.inputEditorHasFocus = CONTEXT_CHAT_INPUT_HAS_FOCUS.bindTo(contextKeyService); - this.history = new HistoryNavigator([], 5); - this._register(this.historyService.onDidClearHistory(() => this.history.clear())); + this.history = this.loadHistory(); + this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2([{ text: '' }], 50, historyKeyFn))); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) { @@ -175,6 +176,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); } + private loadHistory(): HistoryNavigator2 { + const history = this.historyService.getHistory(this.location); + if (history.length === 0) { + history.push({ text: '' }); + } + + return new HistoryNavigator2(history, 50, historyKeyFn); + } + private _getAriaLabel(): string { const verbose = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat); if (verbose) { @@ -184,15 +194,37 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return localize('chatInput', "Chat Input"); } - setState(inputValue: string | undefined): void { - const history = this.historyService.getHistory(this.location); - this.history = new HistoryNavigator(history, 50); + updateState(inputState: Object): void { + if (this.inHistoryNavigation) { + return; + } + + const newEntry = { text: this._inputEditor.getValue(), state: inputState }; + + if (this.history.isAtEnd()) { + // The last history entry should always be the current input value + this.history.replaceLast(newEntry); + } else { + // Added a reference while in the middle of history navigation, it's a new entry + this.history.replaceLast(newEntry); + this.history.resetCursor(); + } + } - if (typeof inputValue === 'string') { - this.setValue(inputValue); + initForNewChatModel(inputValue: string | undefined, inputState: Object): void { + this.history = this.loadHistory(); + this.history.add({ text: inputValue ?? this.history.current().text, state: inputState }); + + if (inputValue) { + this.setValue(inputValue, false); } } + logInputHistory(): void { + const historyStr = [...this.history].map(entry => JSON.stringify(entry)).join('\n'); + this.logService.info(`[${this.location}] Chat input history:`, historyStr); + } + setVisible(visible: boolean): void { this._onDidChangeVisibility.fire(visible); } @@ -202,24 +234,39 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } showPreviousValue(): void { + if (this.history.isAtEnd()) { + this.saveCurrentValue(); + } else { + if (!this.history.has({ text: this._inputEditor.getValue(), state: this.history.current().state })) { + this.saveCurrentValue(); + this.history.resetCursor(); + } + } + this.navigateHistory(true); } showNextValue(): void { + if (this.history.isAtEnd()) { + return; + } else { + if (!this.history.has({ text: this._inputEditor.getValue(), state: this.history.current().state })) { + this.saveCurrentValue(); + this.history.resetCursor(); + } + } + this.navigateHistory(false); } private navigateHistory(previous: boolean): void { - const historyEntry = (previous ? - (this.history.previous() ?? this.history.first()) : this.history.next()) - ?? { text: '' }; - - this.onHistoryEntry = previous || this.history.current() !== null; + const historyEntry = previous ? + this.history.previous() : this.history.next(); aria.status(historyEntry.text); this.inHistoryNavigation = true; - this.setValue(historyEntry.text); + this.setValue(historyEntry.text, true); this.inHistoryNavigation = false; this._onDidLoadInputState.fire(historyEntry.state); @@ -235,10 +282,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - setValue(value: string): void { + setValue(value: string, transient: boolean): void { this.inputEditor.setValue(value); // always leave cursor at the end this.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 }); + + if (!transient) { + this.saveCurrentValue(); + } + } + + private saveCurrentValue(): void { + const newEntry = { text: this._inputEditor.getValue(), state: this.history.current().state }; + this.history.replaceLast(newEntry); } focus() { @@ -253,17 +309,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge * Reset the input and update history. * @param userQuery If provided, this will be added to the history. Followups and programmatic queries should not be passed. */ - async acceptInput(userQuery?: string, inputState?: any): Promise { - if (userQuery) { - let element = this.history.getHistory().find(candidate => candidate.text === userQuery); - if (!element) { - element = { text: userQuery, state: inputState }; - } else { - element.state = inputState; - } - this.history.add(element); + async acceptInput(isUserQuery?: boolean): Promise { + if (isUserQuery) { + const userQuery = this._inputEditor.getValue(); + const entry: IChatHistoryEntry = { text: userQuery, state: this.history.current().state }; + this.history.replaceLast(entry); + this.history.add({ text: '' }); } + this._onDidLoadInputState.fire({}); if (this.accessibilityService.isScreenReaderOptimized() && isMacintosh) { this._acceptInputForVoiceover(); } else { @@ -279,7 +333,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } // Remove the input editor from the DOM temporarily to prevent VoiceOver // from reading the cleared text (the request) to the user. - this._inputEditorElement.removeChild(domNode); + domNode.remove(); this._inputEditor.setValue(''); this._inputEditorElement.appendChild(domNode); this._inputEditor.focus(); @@ -343,21 +397,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._onDidChangeHeight.fire(); } - // Only allow history navigation when the input is empty. - // (If this model change happened as a result of a history navigation, this is canceled out by a call in this.navigateHistory) const model = this._inputEditor.getModel(); const inputHasText = !!model && model.getValue().trim().length > 0; this.inputEditorHasText.set(inputHasText); - - // If the user is typing on a history entry, then reset the onHistoryEntry flag so that history navigation can be disabled - if (!this.inHistoryNavigation) { - this.onHistoryEntry = false; - } - - if (!this.onHistoryEntry) { - this.historyNavigationForewardsEnablement.set(!inputHasText); - this.historyNavigationBackwardsEnablement.set(!inputHasText); - } })); this._register(this._inputEditor.onDidFocusEditorText(() => { this.inputEditorHasFocus.set(true); @@ -379,10 +421,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const atTop = e.position.column === 1 && e.position.lineNumber === 1; this.chatCursorAtTop.set(atTop); - if (this.onHistoryEntry) { - this.historyNavigationBackwardsEnablement.set(atTop); - this.historyNavigationForewardsEnablement.set(e.position.equals(getLastPosition(model))); - } + this.historyNavigationBackwardsEnablement.set(atTop); + this.historyNavigationForewardsEnablement.set(e.position.equals(getLastPosition(model))); })); this.toolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputContainer, this.options.menus.executeToolbar, { @@ -509,6 +549,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (items && items.length > 0) { this.followupsDisposables.add(this.instantiationService.createInstance, ChatFollowups>(ChatFollowups, this.followupsContainer, items, this.location, undefined, followup => this._onDidAcceptFollowup.fire({ followup, response }))); } + this._onDidChangeHeight.fire(); } get contentHeight(): number { @@ -530,6 +571,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const inputEditorHeight = Math.min(data.inputPartEditorHeight, height - data.followupsHeight - data.inputPartVerticalPadding); + const followupsWidth = width - data.inputPartHorizontalPadding; + this.followupsContainer.style.width = `${followupsWidth}px`; + this._inputPartHeight = data.followupsHeight + inputEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight; const initialEditorScrollWidth = this._inputEditor.getScrollWidth(); @@ -565,11 +609,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } saveState(): void { - const inputHistory = this.history.getHistory(); + const inputHistory = [...this.history]; this.historyService.saveHistory(this.location, inputHistory); } } +const historyKeyFn = (entry: IChatHistoryEntry) => JSON.stringify(entry); + function getLastPosition(model: ITextModel): IPosition { return { lineNumber: model.getLineCount(), column: model.getLineLength(model.getLineCount()) + 1 }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 6e29cb90874..21047f36d1c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -7,41 +7,24 @@ import * as dom from 'vs/base/browser/dom'; import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { alert } from 'vs/base/browser/ui/aria/aria'; -import { Button } from 'vs/base/browser/ui/button/button'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; -import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; -import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { IAction } from 'vs/base/common/actions'; -import { distinct } from 'vs/base/common/arrays'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { coalesce, distinct } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; -import { FileAccess, Schemas, matchesSomeScheme } from 'vs/base/common/network'; +import { FileAccess } from 'vs/base/common/network'; import { clamp } from 'vs/base/common/numbers'; import { autorun } from 'vs/base/common/observable'; -import { basename } from 'vs/base/common/path'; -import { basenameOrAuthority, isEqual } from 'vs/base/common/resources'; -import { equalsIgnoreCase } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; -import { Range } from 'vs/editor/common/core/range'; -import { TextEdit } from 'vs/editor/common/languages'; -import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; -import { IModelService } from 'vs/editor/common/services/model'; -import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; -import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; import { localize } from 'vs/nls'; import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; @@ -49,52 +32,53 @@ import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { FileKind, FileType } from 'vs/platform/files/common/files'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { WorkbenchCompressibleAsyncDataTree, WorkbenchList } from 'vs/platform/list/browser/listService'; import { ILogService } from 'vs/platform/log/common/log'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { ChatTreeItem, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; -import { ChatConfirmationWidget } from 'vs/workbench/contrib/chat/browser/chatConfirmationWidget'; +import { ChatCommandButtonContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart'; +import { ChatConfirmationContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { ChatMarkdownContentPart, EditorPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart'; +import { ChatProgressContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart'; +import { ChatReferencesContentPart, ContentReferencesListPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart'; +import { ChatTaskContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart'; +import { ChatTextEditContentPart, DiffEditorPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart'; +import { ChatTreeContentPart, TreePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart'; +import { ChatWarningContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatWarningContentPart'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; +import { ChatMarkdownRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { ChatCodeBlockContentProvider, CodeBlockPart, CodeCompareBlockPart, ICodeBlockData, ICodeCompareBlockData, ICodeCompareBlockDiffData, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { ChatCodeBlockContentProvider } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { ChatAgentLocation, IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatAgentVoteDirection, IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatTask, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatTaskRenderData, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; -import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; -import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; -import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; -import { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../common/annotations'; +import { ChatAgentVoteDirection, IChatConfirmation, IChatFollowup, IChatTask, IChatTreeData } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatReferences, IChatRendererContent, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; +import { annotateSpecialMarkdownContent } from '../common/annotations'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; import { IChatListItemRendererOptions } from './chat'; -import { ChatMarkdownRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownRenderer'; -import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; const $ = dom.$; interface IChatListItemTemplate { currentElement?: ChatTreeItem; + renderedParts?: IChatContentPart[]; readonly rowContainer: HTMLElement; readonly titleToolbar?: MenuWorkbenchToolBar; readonly avatarContainer: HTMLElement; readonly username: HTMLElement; readonly detail: HTMLElement; readonly value: HTMLElement; - readonly referencesListContainer: HTMLElement; readonly contextKeyService: IContextKeyService; + readonly instantiationService: IInstantiationService; readonly templateDisposables: IDisposable; readonly elementDisposables: DisposableStore; readonly agentHover: ChatAgentHover; @@ -105,11 +89,9 @@ interface IItemHeightChangeParams { height: number; } -interface IChatMarkdownRenderResult extends IMarkdownRenderResult { - codeBlockCount: number; -} - -const forceVerboseLayoutTracing = false; +const forceVerboseLayoutTracing = false + // || Boolean("TRUE") // causes a linter warning so that it cannot be pushed + ; export interface IChatRendererDelegate { getListLength(): number; @@ -147,8 +129,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer()); - private _usedReferencesEnabled = false; - constructor( editorOptions: ChatEditorOptions, private readonly location: ChatAgentLocation, @@ -159,14 +139,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - if (e.affectsConfiguration('chat.experimental.usedReferences')) { - this._usedReferencesEnabled = configService.getValue('chat.experimental.usedReferences') ?? true; - } - })); } get templateId(): string { @@ -271,17 +240,39 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - if (isResponseVM(template.currentElement) && template.currentElement.agent) { + if (isResponseVM(template.currentElement) && template.currentElement.agent && !template.currentElement.agent.isDefault) { agentHover.setAgent(template.currentElement.agent.id); return agentHover.domNode; } @@ -316,7 +307,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer isResponseVM(template.currentElement) ? template.currentElement.agent : undefined, this.commandService); - templateDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), user, hoverContent, hoverOptions)); + templateDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), user, hoverContent, hoverOptions)); templateDisposables.add(dom.addDisposableListener(user, dom.EventType.KEY_DOWN, e => { const ev = new StandardKeyboardEvent(e); if (ev.equals(KeyCode.Space) || ev.equals(KeyCode.Enter)) { @@ -328,7 +319,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { try { - if (this.doNextProgressiveRender(element, index, templateData, !!initial, progressiveRenderingDisposables)) { + if (this.doNextProgressiveRender(element, index, templateData, !!initial)) { timer.cancel(); } } catch (err) { // Kill the timer if anything went wrong, avoid getting stuck in a nasty rendering loop. timer.cancel(); - throw err; + this.logService.error(err); } }; timer.cancelAndSet(runProgressiveRender, 50, dom.getWindow(templateData.rowContainer)); runProgressiveRender(true); } else if (isResponseVM(element)) { - const renderableResponse = annotateSpecialMarkdownContent(element.response.value); - this.basicRenderElement(renderableResponse, element, index, templateData); + this.basicRenderElement(element, index, templateData); } else if (isRequestVM(element)) { - const markdown = 'message' in element.message ? - element.message.message : - this.markdownDecorationsRenderer.convertParsedRequestToMarkdown(element.message); - this.basicRenderElement([{ content: new MarkdownString(markdown), kind: 'markdownContent' }], element, index, templateData); + this.basicRenderElement(element, index, templateData); } else { this.renderWelcomeMessage(element, templateData); } @@ -423,13 +408,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer, element: ChatTreeItem, index: number, templateData: IChatListItemTemplate) { - const fillInIncompleteTokens = isResponseVM(element) && (!element.isComplete || element.isCanceled || element.errorDetails?.responseIsFiltered || element.errorDetails?.responseIsIncomplete); + private basicRenderElement(element: ChatTreeItem, index: number, templateData: IChatListItemTemplate) { + let value: IChatRendererContent[] = []; + if (isRequestVM(element)) { + const markdown = 'message' in element.message ? + element.message.message : + this.markdownDecorationsRenderer.convertParsedRequestToMarkdown(element.message); + value = [{ content: new MarkdownString(markdown), kind: 'markdownContent' }]; + } else if (isResponseVM(element)) { + value = annotateSpecialMarkdownContent(element.response.value); + if (element.contentReferences.length) { + value.unshift({ kind: 'references', references: element.contentReferences }); + } + } dom.clearNode(templateData.value); - dom.clearNode(templateData.referencesListContainer); if (isResponseVM(element)) { this.renderDetail(element, templateData); } - this.renderContentReferencesIfNeeded(element, templateData, templateData.elementDisposables); - - let fileTreeIndex = 0; - let codeBlockIndex = 0; + const parts: IChatContentPart[] = []; value.forEach((data, index) => { - const result = data.kind === 'treeData' - ? this.renderTreeData(data.treeData, element, templateData, fileTreeIndex++) - : data.kind === 'markdownContent' - ? this.renderMarkdown(data.content, element, templateData, fillInIncompleteTokens, codeBlockIndex) - : data.kind === 'progressMessage' && onlyProgressMessagesAfterI(value, index) ? this.renderProgressMessage(data, false) // TODO render command - : data.kind === 'progressTask' ? this.renderProgressTask(data, false, element, templateData) - : data.kind === 'command' ? this.renderCommandButton(element, data) - : data.kind === 'textEditGroup' ? this.renderTextEdit(element, data, templateData) - : data.kind === 'warning' ? this.renderNotification('warning', data.content) - : data.kind === 'confirmation' ? this.renderConfirmation(element, data, templateData) - : undefined; - - if (result) { - templateData.value.appendChild(result.element); - templateData.elementDisposables.add(result); - - if ('codeBlockCount' in result) { - codeBlockIndex += (result as IChatMarkdownRenderResult).codeBlockCount; - } + const context: IChatContentPartRenderContext = { + element, + index, + content: value, + preceedingContentParts: parts, + }; + const newPart = this.renderChatContentPart(data, templateData, context); + if (newPart) { + templateData.value.appendChild(newPart.domNode); + parts.push(newPart); } }); + templateData.renderedParts = parts; if (isResponseVM(element) && element.errorDetails?.message) { - const renderedError = this.renderNotification(element.errorDetails.responseIsFiltered ? 'info' : 'error', new MarkdownString(element.errorDetails.message)); + const renderedError = this.instantiationService.createInstance(ChatWarningContentPart, element.errorDetails.responseIsFiltered ? 'info' : 'error', new MarkdownString(element.errorDetails.message), this.renderer); templateData.elementDisposables.add(renderedError); - templateData.value.appendChild(renderedError.element); + templateData.value.appendChild(renderedError.domNode); } const newHeight = templateData.rowContainer.offsetHeight; @@ -541,10 +521,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { if (Array.isArray(item)) { const scopedInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, templateData.contextKeyService])); templateData.elementDisposables.add( @@ -556,11 +534,18 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this._onDidClickFollowup.fire(followup))); } else { - const result = this.renderMarkdown(item as IMarkdownString, element, templateData); - templateData.value.appendChild(result.element); + const context: IChatContentPartRenderContext = { + element, + index: i, + // NA for welcome msg + content: [], + preceedingContentParts: [] + }; + const result = this.renderMarkdown(item, templateData, context); + templateData.value.appendChild(result.domNode); templateData.elementDisposables.add(result); } - } + }); const newHeight = templateData.rowContainer.offsetHeight; const fireEvent = !element.currentRenderedHeight || element.currentRenderedHeight !== newHeight; @@ -579,172 +564,33 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - const renderedPart = renderedParts[index]; - // Is this part completely new? - if (!renderedPart) { - if (part.kind === 'treeData') { - partsToRender[index] = part.treeData; - } else if (part.kind === 'progressMessage') { - partsToRender[index] = { - progressMessage: part, - isAtEndOfResponse: onlyProgressMessagesAfterI(renderableResponse, index), - isLast: index === renderableResponse.length - 1, - } satisfies IChatProgressMessageRenderData; - } else if (part.kind === 'command' || - part.kind === 'textEditGroup' || - part.kind === 'confirmation' || - part.kind === 'warning') { - partsToRender[index] = part; - } else if (part.kind === 'progressTask') { - partsToRender[index] = { - task: part, - isSettled: part.isSettled?.() ?? true, - progressLength: part.progress.length, - }; - } else { - const wordCountResult = this.getDataForProgressiveRender(element, contentToMarkdown(part.content), { renderedWordCount: 0, lastRenderTime: 0 }); - if (wordCountResult !== undefined) { - this.traceLayout('doNextProgressiveRender', `Rendering new part ${index}, wordCountResult=${wordCountResult.actualWordCount}, rate=${wordCountResult.rate}`); - partsToRender[index] = { - renderedWordCount: wordCountResult.actualWordCount, - lastRenderTime: Date.now(), - isFullyRendered: wordCountResult.isFullString, - originalMarkdown: part.content, - }; - wordCountResults[index] = wordCountResult; - } - } - } - - // Did this part's content change? - else if ((part.kind === 'markdownContent' || part.kind === 'progressMessage') && isMarkdownRenderData(renderedPart)) { // TODO - const wordCountResult = this.getDataForProgressiveRender(element, contentToMarkdown(part.content), renderedPart); - // Check if there are any new words to render - if (wordCountResult !== undefined && renderedPart.renderedWordCount !== wordCountResult?.actualWordCount) { - this.traceLayout('doNextProgressiveRender', `Rendering changed part ${index}, wordCountResult=${wordCountResult.actualWordCount}, rate=${wordCountResult.rate}`); - partsToRender[index] = { - renderedWordCount: wordCountResult.actualWordCount, - lastRenderTime: Date.now(), - isFullyRendered: wordCountResult.isFullString, - originalMarkdown: part.content, - }; - wordCountResults[index] = wordCountResult; - } else if (!renderedPart.isFullyRendered && !wordCountResult) { - // This part is not fully rendered, but not enough time has passed to render more content - somePartIsNotFullyRendered = true; - } - } - - // Is it a progress message that needs to be rerendered? - else if (part.kind === 'progressMessage' && isProgressMessageRenderData(renderedPart) && ( - (renderedPart.isAtEndOfResponse !== onlyProgressMessagesAfterI(renderableResponse, index)) || - renderedPart.isLast !== (index === renderableResponse.length - 1))) { - partsToRender[index] = { - progressMessage: part, - isAtEndOfResponse: onlyProgressMessagesAfterI(renderableResponse, index), - isLast: index === renderableResponse.length - 1, - } satisfies IChatProgressMessageRenderData; - } - - else if (part.kind === 'progressTask' && isProgressTaskRenderData(renderedPart)) { - const isSettled = part.isSettled?.() ?? true; - if (renderedPart.isSettled !== isSettled || part.progress.length !== renderedPart.progressLength || isSettled) { - partsToRender[index] = { task: part, isSettled, progressLength: part.progress.length }; - } - } - }); - - isFullyRendered = partsToRender.filter((p) => !('isSettled' in p) || !p.isSettled).length === 0 && !somePartIsNotFullyRendered; + this.traceLayout('doNextProgressiveRender', `START progressive render, index=${index}, renderData=${JSON.stringify(element.renderData)}`); + const contentForThisTurn = this.getNextProgressiveRenderContent(element); + const partsToRender = this.diff(templateData.renderedParts ?? [], contentForThisTurn, element); + isFullyRendered = partsToRender.every(part => part === null); if (isFullyRendered && element.isComplete) { // Response is done and content is rendered, so do a normal render - this.traceLayout('runProgressiveRender', `end progressive render, index=${index} and clearing renderData, response is complete, index=${index}`); + this.traceLayout('doNextProgressiveRender', `END progressive render, index=${index} and clearing renderData, response is complete`); element.renderData = undefined; - disposables.clear(); - this.basicRenderElement(renderableResponse, element, index, templateData); + this.basicRenderElement(element, index, templateData); + // TODO return here } else if (!isFullyRendered) { - disposables.clear(); - this.renderContentReferencesIfNeeded(element, templateData, disposables); - let hasRenderedOneMarkdownBlock = false; - partsToRender.forEach((partToRender, index) => { - if (!partToRender) { - return; - } - - // Undefined => don't do anything. null => remove the rendered element - let result: { element: HTMLElement } & IDisposable | undefined | null; - if (isInteractiveProgressTreeData(partToRender)) { - result = this.renderTreeData(partToRender, element, templateData, index); - } else if (isProgressMessageRenderData(partToRender)) { - if (onlyProgressMessageRenderDatasAfterI(partsToRender, index)) { - result = this.renderProgressMessage(partToRender.progressMessage, index === partsToRender.length - 1); - } else { - result = null; - } - } else if (isProgressTaskRenderData(partToRender)) { - result = this.renderProgressTask(partToRender.task, !partToRender.isSettled, element, templateData); - } else if (isCommandButtonRenderData(partToRender)) { - result = this.renderCommandButton(element, partToRender); - } else if (isTextEditRenderData(partToRender)) { - result = this.renderTextEdit(element, partToRender, templateData); - } else if (isConfirmationRenderData(partToRender)) { - result = this.renderConfirmation(element, partToRender, templateData); - } else if (isWarningRenderData(partToRender)) { - result = this.renderNotification('warning', partToRender.content); - } - - // Avoid doing progressive rendering for multiple markdown parts simultaneously - else if (!hasRenderedOneMarkdownBlock && wordCountResults[index]) { - const { value } = wordCountResults[index]; - const part = partsToRender[index]; - const originalMarkdown = 'originalMarkdown' in part ? part.originalMarkdown : undefined; - const markdownToRender = new MarkdownString(value, originalMarkdown); - result = this.renderMarkdown(markdownToRender, element, templateData, true); - hasRenderedOneMarkdownBlock = true; - } - - if (result === undefined) { - return; - } - - // Doing the progressive render - renderedParts[index] = partToRender; - const existingElement = templateData.value.children[index]; - if (existingElement) { - if (result === null) { - templateData.value.replaceChild($('span.placeholder-for-deleted-thing'), existingElement); - } else { - templateData.value.replaceChild(result.element, existingElement); - } - } else if (result) { - templateData.value.appendChild(result.element); - } - - if (result) { - disposables.add(result); - } - }); + this.traceLayout('doNextProgressiveRender', `doing progressive render, ${partsToRender.length} parts to render`); + this.renderChatContentDiff(partsToRender, contentForThisTurn, element, templateData); } else { // Nothing new to render, not done, keep waiting return false; @@ -761,487 +607,265 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer void } { - const treeDisposables = new DisposableStore(); - const ref = treeDisposables.add(this._treePool.get()); - const tree = ref.object; - - treeDisposables.add(tree.onDidOpen((e) => { - if (e.element && !('children' in e.element)) { - this.openerService.open(e.element.uri); + private renderChatContentDiff(partsToRender: ReadonlyArray, contentForThisTurn: ReadonlyArray, element: IChatResponseViewModel, templateData: IChatListItemTemplate): void { + const renderedParts = templateData.renderedParts ?? []; + templateData.renderedParts = renderedParts; + partsToRender.forEach((partToRender, index) => { + if (!partToRender) { + // null=no change + return; } - })); - treeDisposables.add(tree.onDidChangeCollapseState(() => { - this.updateItemHeight(templateData); - })); - treeDisposables.add(tree.onContextMenu((e) => { - e.browserEvent.preventDefault(); - e.browserEvent.stopPropagation(); - })); - tree.setInput(data).then(() => { - if (!ref.isStale()) { - tree.layout(); - this.updateItemHeight(templateData); + const alreadyRenderedPart = templateData.renderedParts?.[index]; + if (alreadyRenderedPart) { + alreadyRenderedPart.dispose(); } - }); - if (isResponseVM(element)) { - const fileTreeFocusInfo = { - treeDataId: data.uri.toString(), - treeIndex: treeDataIndex, - focus() { - tree.domFocus(); - } + const preceedingContentParts = renderedParts.slice(0, index); + const context: IChatContentPartRenderContext = { + element, + content: contentForThisTurn, + preceedingContentParts, + index }; + const newPart = this.renderChatContentPart(partToRender, templateData, context); + if (newPart) { + // Maybe the part can't be rendered in this context, but this shouldn't really happen + if (alreadyRenderedPart) { + try { + // This method can throw HierarchyRequestError + alreadyRenderedPart.domNode.replaceWith(newPart.domNode); + } catch (err) { + this.logService.error('ChatListItemRenderer#renderChatContentDiff: error replacing part', err); + } + } else { + templateData.value.appendChild(newPart.domNode); + } - treeDisposables.add(tree.onDidFocus(() => { - this.focusedFileTreesByResponseId.set(element.id, fileTreeFocusInfo.treeIndex); - })); - - const fileTrees = this.fileTreesByResponseId.get(element.id) ?? []; - fileTrees.push(fileTreeFocusInfo); - this.fileTreesByResponseId.set(element.id, distinct(fileTrees, (v) => v.treeDataId)); - treeDisposables.add(toDisposable(() => this.fileTreesByResponseId.set(element.id, fileTrees.filter(v => v.treeDataId !== data.uri.toString())))); - } - - return { - element: tree.getHTMLElement().parentElement!, - dispose: () => { - treeDisposables.dispose(); - } - }; - } - - private renderContentReferencesIfNeeded(element: ChatTreeItem, templateData: IChatListItemTemplate, disposables: DisposableStore): void { - if (isResponseVM(element) && this._usedReferencesEnabled && element.contentReferences.length) { - dom.show(templateData.referencesListContainer); - const contentReferencesListResult = this.renderContentReferencesListData(null, element.contentReferences, element, templateData); - if (templateData.referencesListContainer.firstChild) { - templateData.referencesListContainer.replaceChild(contentReferencesListResult.element, templateData.referencesListContainer.firstChild!); - } else { - templateData.referencesListContainer.appendChild(contentReferencesListResult.element); + renderedParts[index] = newPart; + } else if (alreadyRenderedPart) { + alreadyRenderedPart.domNode.remove(); } - disposables.add(contentReferencesListResult); - } else { - dom.hide(templateData.referencesListContainer); - } + }); } - private renderContentReferencesListData(task: IChatTask | null, data: ReadonlyArray, element: IChatResponseViewModel, templateData: IChatListItemTemplate): { element: HTMLElement; dispose: () => void } { - const listDisposables = new DisposableStore(); - const referencesLabel = task?.content.value ?? (data.length > 1 ? - localize('usedReferencesPlural', "Used {0} references", data.length) : - localize('usedReferencesSingular', "Used {0} reference", 1)); - const iconElement = $('.chat-used-context-icon'); - const icon = (element: IChatResponseViewModel) => element.usedReferencesExpanded ? Codicon.chevronDown : Codicon.chevronRight; - iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); - const buttonElement = $('.chat-used-context-label', undefined); - - const collapseButton = listDisposables.add(new Button(buttonElement, { - buttonBackground: undefined, - buttonBorder: undefined, - buttonForeground: undefined, - buttonHoverBackground: undefined, - buttonSecondaryBackground: undefined, - buttonSecondaryForeground: undefined, - buttonSecondaryHoverBackground: undefined, - buttonSeparator: undefined - })); - const container = $('.chat-used-context', undefined, buttonElement); - collapseButton.label = referencesLabel; - collapseButton.element.prepend(iconElement); - this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); - container.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); - listDisposables.add(collapseButton.onDidClick(() => { - iconElement.classList.remove(...ThemeIcon.asClassNameArray(icon(element))); - element.usedReferencesExpanded = !element.usedReferencesExpanded; - iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); - container.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); - this.updateItemHeight(templateData); - this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); - })); + /** + * Returns all content parts that should be rendered, and trimmed markdown content. We will diff this with the current rendered set. + */ + private getNextProgressiveRenderContent(element: IChatResponseViewModel): IChatRendererContent[] { + const data = this.getDataForProgressiveRender(element); - const ref = listDisposables.add(this._contentReferencesListPool.get()); - const list = ref.object; - container.appendChild(list.getHTMLElement().parentElement!); - - listDisposables.add(list.onDidOpen((e) => { - if (e.element && 'reference' in e.element) { - const uriOrLocation = 'variableName' in e.element.reference ? e.element.reference.value : e.element.reference; - const uri = URI.isUri(uriOrLocation) ? uriOrLocation : - uriOrLocation?.uri; - if (uri) { - this.openerService.open( - uri, - { - fromUserGesture: true, - editorOptions: { - ...e.editorOptions, - ...{ - selection: uriOrLocation && 'range' in uriOrLocation ? uriOrLocation.range : undefined - } - } - }); - } - } - })); - listDisposables.add(list.onContextMenu((e) => { - e.browserEvent.preventDefault(); - e.browserEvent.stopPropagation(); - })); + const renderableResponse = annotateSpecialMarkdownContent(element.response.value); - const maxItemsShown = 6; - const itemsShown = Math.min(data.length, maxItemsShown); - const height = itemsShown * 22; - list.layout(height); - list.getHTMLElement().style.height = `${height}px`; - list.splice(0, list.length, data); + this.traceLayout('getNextProgressiveRenderContent', `Want to render ${data.numWordsToRender}, counting...`); + let numNeededWords = data.numWordsToRender; + const partsToRender: IChatRendererContent[] = []; + if (element.contentReferences.length) { + partsToRender.push({ kind: 'references', references: element.contentReferences }); + } - return { - element: container, - dispose: () => { - listDisposables.dispose(); + for (const part of renderableResponse) { + if (numNeededWords <= 0) { + break; } - }; - } - private updateAriaLabel(element: HTMLElement, label: string, expanded?: boolean): void { - element.ariaLabel = expanded ? localize('usedReferencesExpanded', "{0}, expanded", label) : localize('usedReferencesCollapsed', "{0}, collapsed", label); - } + if (part.kind === 'markdownContent') { + const wordCountResult = getNWords(part.content.value, numNeededWords); + if (wordCountResult.isFullString) { + partsToRender.push(part); + } else { + partsToRender.push({ kind: 'markdownContent', content: new MarkdownString(wordCountResult.value, part.content) }); + } - private renderProgressTask(task: IChatTask, showSpinner: boolean, element: ChatTreeItem, templateData: IChatListItemTemplate): IMarkdownRenderResult | undefined { - if (!isResponseVM(element)) { - return; + this.traceLayout('getNextProgressiveRenderContent', ` Want to render ${numNeededWords} words and found ${wordCountResult.returnedWordCount} words. Total words in chunk: ${wordCountResult.totalWordCount}`); + numNeededWords -= wordCountResult.returnedWordCount; + } else { + partsToRender.push(part); + } } - if (task.progress.length) { - const refs = this.renderContentReferencesListData(task, task.progress, element, templateData); - const node = dom.$('.chat-progress-task'); - node.appendChild(refs.element); - return { element: node, dispose: refs.dispose }; + this.traceLayout('getNextProgressiveRenderContent', `Want to render ${data.numWordsToRender} words and ${data.numWordsToRender - numNeededWords} words available`); + const newRenderedWordCount = data.numWordsToRender - numNeededWords; + if (newRenderedWordCount !== element.renderData?.renderedWordCount) { + // Only update lastRenderTime when we actually render new content + element.renderData = { lastRenderTime: Date.now(), renderedWordCount: newRenderedWordCount, renderedParts: partsToRender }; } - return this.renderProgressMessage(task, showSpinner); + return partsToRender; } - private renderProgressMessage(progress: IChatProgressMessage | IChatTask, showSpinner: boolean): IMarkdownRenderResult { - if (showSpinner) { - // this step is in progress, communicate it to SR users - alert(progress.content.value); - } - const codicon = showSpinner ? ThemeIcon.modify(Codicon.loading, 'spin').id : Codicon.check.id; - const markdown = new MarkdownString(`$(${codicon}) ${progress.content.value}`, { - supportThemeIcons: true - }); - const result = this.renderer.render(markdown); - result.element.classList.add('progress-step'); - return result; - } + private getDataForProgressiveRender(element: IChatResponseViewModel) { + const renderData = element.renderData ?? { lastRenderTime: 0, renderedWordCount: 0 }; - private renderCommandButton(element: ChatTreeItem, commandButton: IChatCommandButton): IMarkdownRenderResult { - const container = $('.chat-command-button'); - const disposables = new DisposableStore(); - const enabled = !isResponseVM(element) || !element.isStale; - const tooltip = enabled ? - commandButton.command.tooltip : - localize('commandButtonDisabled', "Button not available in restored chat"); - const button = disposables.add(new Button(container, { ...defaultButtonStyles, supportIcons: true, title: tooltip })); - button.label = commandButton.command.title; - button.enabled = enabled; - - // TODO still need telemetry for command buttons - disposables.add(button.onDidClick(() => this.commandService.executeCommand(commandButton.command.id, ...(commandButton.command.arguments ?? [])))); - return { - dispose() { - disposables.dispose(); - }, - element: container - }; - } + const rate = this.getProgressiveRenderRate(element); + const numWordsToRender = renderData.lastRenderTime === 0 ? + 1 : + renderData.renderedWordCount + + // Additional words to render beyond what's already rendered + Math.floor((Date.now() - renderData.lastRenderTime) / 1000 * rate); - private renderNotification(kind: 'info' | 'warning' | 'error', content: IMarkdownString): IMarkdownRenderResult { - const container = $('.chat-notification-widget'); - let icon; - let iconClass; - switch (kind) { - case 'warning': - icon = Codicon.warning; - iconClass = '.chat-warning-codicon'; - break; - case 'error': - icon = Codicon.error; - iconClass = '.chat-error-codicon'; - break; - case 'info': - icon = Codicon.info; - iconClass = '.chat-info-codicon'; - break; - } - container.appendChild($(iconClass, undefined, renderIcon(icon))); - const markdownContent = this.renderer.render(content); - container.appendChild(markdownContent.element); return { - element: container, - dispose() { markdownContent.dispose(); } + numWordsToRender, + rate }; } - private renderConfirmation(element: ChatTreeItem, confirmation: IChatConfirmation, templateData: IChatListItemTemplate): IMarkdownRenderResult | undefined { - const store = new DisposableStore(); - const confirmationWidget = store.add(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, confirmation.message, [ - { label: localize('accept', "Accept"), data: confirmation.data }, - { label: localize('dismiss', "Dismiss"), data: confirmation.data, isSecondary: true }, - ])); - confirmationWidget.setShowButtons(!confirmation.isUsed); - - store.add(confirmationWidget.onDidClick(async e => { - if (isResponseVM(element)) { - const prompt = `${e.label}: "${confirmation.title}"`; - const data: IChatSendRequestOptions = e.isSecondary ? - { rejectedConfirmationData: [e.data] } : - { acceptedConfirmationData: [e.data] }; - data.agentId = element.agent?.id; - if (await this.chatService.sendRequest(element.sessionId, prompt, data)) { - confirmation.isUsed = true; - confirmationWidget.setShowButtons(false); - this.updateItemHeight(templateData); - } + private diff(renderedParts: ReadonlyArray, contentToRender: ReadonlyArray, element: ChatTreeItem): ReadonlyArray { + const diff: (IChatRendererContent | null)[] = []; + for (let i = 0; i < contentToRender.length; i++) { + const content = contentToRender[i]; + const renderedPart = renderedParts[i]; + + if (!renderedPart || !renderedPart.hasSameContent(content, contentToRender.slice(i + 1), element)) { + diff.push(content); + } else { + // null -> no change + diff.push(null); } - })); + } - return { - element: confirmationWidget.domNode, - dispose() { store.dispose(); } - }; + return diff; } - private renderTextEdit(element: ChatTreeItem, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IMarkdownRenderResult | undefined { - - // TODO@jrieken move this into the CompareCodeBlock and properly say what kind of changes happen - if (this.rendererOptions.renderTextEditsAsSummary?.(chatTextEdit.uri)) { - if (isResponseVM(element) && element.response.value.every(item => item.kind === 'textEditGroup')) { - return { - element: $('.interactive-edits-summary', undefined, !element.isComplete ? localize('editsSummary1', "Making changes...") : localize('editsSummary', "Made changes.")), - dispose() { } - }; - } - return undefined; + private renderChatContentPart(content: IChatRendererContent, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { + if (content.kind === 'treeData') { + return this.renderTreeData(content, templateData, context); + } else if (content.kind === 'progressMessage') { + return this.instantiationService.createInstance(ChatProgressContentPart, content, this.renderer, context); + } else if (content.kind === 'progressTask') { + return this.renderProgressTask(content, templateData, context); + } else if (content.kind === 'command') { + return this.instantiationService.createInstance(ChatCommandButtonContentPart, content, context); + } else if (content.kind === 'textEditGroup') { + return this.renderTextEdit(context, content, templateData); + } else if (content.kind === 'confirmation') { + return this.renderConfirmation(context, content, templateData); + } else if (content.kind === 'warning') { + return this.instantiationService.createInstance(ChatWarningContentPart, 'warning', content.content, this.renderer); + } else if (content.kind === 'markdownContent') { + return this.renderMarkdown(content.content, templateData, context); + } else if (content.kind === 'references') { + return this.renderContentReferencesListData(content, undefined, context, templateData); } - const store = new DisposableStore(); - const cts = new CancellationTokenSource(); - - let isDisposed = false; - store.add(toDisposable(() => { - isDisposed = true; - cts.dispose(true); - })); + return undefined; + } - const ref = this._diffEditorPool.get(); + private renderTreeData(content: IChatTreeData, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart { + const data = content.treeData; + const treeDataIndex = context.preceedingContentParts.filter(part => part instanceof ChatTreeContentPart).length; + const treePart = this.instantiationService.createInstance(ChatTreeContentPart, data, context.element, this._treePool, treeDataIndex); - // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) - // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) - store.add(ref.object.onDidChangeContentHeight(() => { - ref.object.layout(this._currentLayoutWidth); + treePart.addDisposable(treePart.onDidChangeHeight(() => { this.updateItemHeight(templateData); })); - const data: ICodeCompareBlockData = { - element, - edit: chatTextEdit, - diffData: (async () => { - - const ref = await this.textModelService.createModelReference(chatTextEdit.uri); - - if (isDisposed) { - ref.dispose(); - return; - } - - store.add(ref); - - const original = ref.object.textEditorModel; - let originalSha1: string = ''; - - if (chatTextEdit.state) { - originalSha1 = chatTextEdit.state.sha1; - } else { - const sha1 = new DefaultModelSHA1Computer(); - if (sha1.canComputeSHA1(original)) { - originalSha1 = sha1.computeSHA1(original); - chatTextEdit.state = { sha1: originalSha1, applied: 0 }; - } - } - - const modified = this.modelService.createModel( - createTextBufferFactoryFromSnapshot(original.createSnapshot()), - { languageId: original.getLanguageId(), onDidChange: Event.None }, - URI.from({ scheme: Schemas.vscodeChatCodeBlock, path: original.uri.path, query: generateUuid() }), - false - ); - const modRef = await this.textModelService.createModelReference(modified.uri); - store.add(modRef); - - const editGroups: ISingleEditOperation[][] = []; - if (isResponseVM(element)) { - const chatModel = this.chatService.getSession(element.sessionId)!; - - for (const request of chatModel.getRequests()) { - if (!request.response) { - continue; - } - for (const item of request.response.response.value) { - if (item.kind !== 'textEditGroup' || item.state?.applied || !isEqual(item.uri, chatTextEdit.uri)) { - continue; - } - for (const group of item.edits) { - const edits = group.map(TextEdit.asEditOperation); - editGroups.push(edits); - } - } - if (request.response === element.model) { - break; - } - } + if (isResponseVM(context.element)) { + const fileTreeFocusInfo = { + treeDataId: data.uri.toString(), + treeIndex: treeDataIndex, + focus() { + treePart.domFocus(); } + }; - for (const edits of editGroups) { - modified.pushEditOperations(null, edits, () => null); - } + // TODO@roblourens there's got to be a better way to navigate trees + treePart.addDisposable(treePart.onDidFocus(() => { + this.focusedFileTreesByResponseId.set(context.element.id, fileTreeFocusInfo.treeIndex); + })); - return { - modified, - original, - originalSha1 - } satisfies ICodeCompareBlockDiffData; - })() - }; - ref.object.render(data, this._currentLayoutWidth, cts.token); + const fileTrees = this.fileTreesByResponseId.get(context.element.id) ?? []; + fileTrees.push(fileTreeFocusInfo); + this.fileTreesByResponseId.set(context.element.id, distinct(fileTrees, (v) => v.treeDataId)); + treePart.addDisposable(toDisposable(() => this.fileTreesByResponseId.set(context.element.id, fileTrees.filter(v => v.treeDataId !== data.uri.toString())))); + } - return { - element: ref.object.element, - dispose() { - store.dispose(); - ref.dispose(); - }, - }; + return treePart; } - private renderMarkdown(markdown: IMarkdownString, element: ChatTreeItem, templateData: IChatListItemTemplate, fillInIncompleteTokens = false, codeBlockStartIndex = 0): IChatMarkdownRenderResult { - const disposables = new DisposableStore(); - - // We release editors in order so that it's more likely that the same editor will be assigned if this element is re-rendered right away, like it often is during progressive rendering - const orderedDisposablesList: IDisposable[] = []; - const codeblocks: IChatCodeBlockInfo[] = []; - let codeBlockIndex = codeBlockStartIndex; - const result = this.renderer.render(markdown, { - fillInIncompleteTokens, - codeBlockRendererSync: (languageId, text) => { - const index = codeBlockIndex++; - let textModel: Promise; - let range: Range | undefined; - let vulns: readonly IMarkdownVulnerability[] | undefined; - if (equalsIgnoreCase(languageId, localFileLanguageId)) { - try { - const parsedBody = parseLocalFileData(text); - range = parsedBody.range && Range.lift(parsedBody.range); - textModel = this.textModelService.createModelReference(parsedBody.uri).then(ref => ref.object); - } catch (e) { - return $('div'); - } - } else { - if (!isRequestVM(element) && !isResponseVM(element)) { - console.error('Trying to render code block in welcome', element.id, index); - return $('div'); - } - - const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; - const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, index); - vulns = modelEntry.vulns; - textModel = modelEntry.model; - } + private renderContentReferencesListData(references: IChatReferences, labelOverride: string | undefined, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): ChatReferencesContentPart { + const referencesPart = this.instantiationService.createInstance(ChatReferencesContentPart, references.references, labelOverride, context.element as IChatResponseViewModel, this._contentReferencesListPool); + referencesPart.addDisposable(referencesPart.onDidChangeHeight(() => { + this.updateItemHeight(templateData); + })); - const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; - const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: templateData.contextKeyService, vulns }, text); - - // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) - // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) - disposables.add(ref.object.onDidChangeContentHeight(() => { - ref.object.layout(this._currentLayoutWidth); - this.updateItemHeight(templateData); - })); - - if (isResponseVM(element)) { - const info: IChatCodeBlockInfo = { - codeBlockIndex: index, - element, - focus() { - ref.object.focus(); - } - }; - codeblocks.push(info); - if (ref.object.uri) { - const uri = ref.object.uri; - this.codeBlocksByEditorUri.set(uri, info); - disposables.add(toDisposable(() => this.codeBlocksByEditorUri.delete(uri))); - } - } - orderedDisposablesList.push(ref); - return ref.object.element; - }, - asyncRenderCallback: () => this.updateItemHeight(templateData), - }); + return referencesPart; + } - if (isResponseVM(element)) { - this.codeBlocksByResponseId.set(element.id, codeblocks); - disposables.add(toDisposable(() => this.codeBlocksByResponseId.delete(element.id))); + private renderProgressTask(task: IChatTask, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { + if (!isResponseVM(context.element)) { + return; } - disposables.add(this.markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(result.element)); - - orderedDisposablesList.reverse().forEach(d => disposables.add(d)); - return { - codeBlockCount: codeBlockIndex - codeBlockStartIndex, - element: result.element, - dispose() { - result.dispose(); - disposables.dispose(); - } - }; + const taskPart = this.instantiationService.createInstance(ChatTaskContentPart, task, this._contentReferencesListPool, this.renderer, context); + taskPart.addDisposable(taskPart.onDidChangeHeight(() => { + this.updateItemHeight(templateData); + })); + return taskPart; } - private renderCodeBlock(data: ICodeBlockData, text: string): IDisposableReference { - const ref = this._editorPool.get(); - const editorInfo = ref.object; - if (isResponseVM(data.element)) { - this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }); - } + private renderConfirmation(context: IChatContentPartRenderContext, confirmation: IChatConfirmation, templateData: IChatListItemTemplate): IChatContentPart { + const part = this.instantiationService.createInstance(ChatConfirmationContentPart, confirmation, context); + part.addDisposable(part.onDidChangeHeight(() => this.updateItemHeight(templateData))); + return part; + } - editorInfo.render(data, this._currentLayoutWidth, this.rendererOptions.editableCodeBlock); + private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart { + const textEditPart = this.instantiationService.createInstance(ChatTextEditContentPart, chatTextEdit, context, this.rendererOptions, this._diffEditorPool, this._currentLayoutWidth); + textEditPart.addDisposable(textEditPart.onDidChangeHeight(() => { + textEditPart.layout(this._currentLayoutWidth); + this.updateItemHeight(templateData); + })); - return ref; + return textEditPart; } - private getDataForProgressiveRender(element: IChatResponseViewModel, data: IMarkdownString, renderData: Pick): IWordCountResult & { rate: number } | undefined { - const rate = this.getProgressiveRenderRate(element); - const numWordsToRender = renderData.lastRenderTime === 0 ? - 1 : - renderData.renderedWordCount + - // Additional words to render beyond what's already rendered - Math.floor((Date.now() - renderData.lastRenderTime) / 1000 * rate); + private renderMarkdown(markdown: IMarkdownString, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart { + const element = context.element; + const fillInIncompleteTokens = isResponseVM(element) && (!element.isComplete || element.isCanceled || element.errorDetails?.responseIsFiltered || element.errorDetails?.responseIsIncomplete || !!element.renderData); + const codeBlockStartIndex = context.preceedingContentParts.reduce((acc, part) => acc + (part instanceof ChatMarkdownContentPart ? part.codeblocks.length : 0), 0); + const markdownPart = this.instantiationService.createInstance(ChatMarkdownContentPart, markdown, context, this._editorPool, fillInIncompleteTokens, codeBlockStartIndex, this.renderer, this._currentLayoutWidth, this.codeBlockModelCollection, this.rendererOptions); + markdownPart.addDisposable(markdownPart.onDidChangeHeight(() => { + markdownPart.layout(this._currentLayoutWidth); + this.updateItemHeight(templateData); + })); - if (numWordsToRender === renderData.renderedWordCount) { - return undefined; - } + const codeBlocksByResponseId = this.codeBlocksByResponseId.get(element.id) ?? []; + this.codeBlocksByResponseId.set(element.id, codeBlocksByResponseId); + markdownPart.addDisposable(toDisposable(() => { + const codeBlocksByResponseId = this.codeBlocksByResponseId.get(element.id); + if (codeBlocksByResponseId) { + markdownPart.codeblocks.forEach((info, i) => delete codeBlocksByResponseId[codeBlockStartIndex + i]); + } + })); - return { - ...getNWords(data.value, numWordsToRender), - rate - }; + markdownPart.codeblocks.forEach((info, i) => { + codeBlocksByResponseId[codeBlockStartIndex + i] = info; + if (info.uri) { + const uri = info.uri; + this.codeBlocksByEditorUri.set(uri, info); + markdownPart.addDisposable(toDisposable(() => this.codeBlocksByEditorUri.delete(uri))); + } + }); + + return markdownPart; } disposeElement(node: ITreeNode, index: number, templateData: IChatListItemTemplate): void { + this.traceLayout('disposeElement', `Disposing element, index=${index}`); + + // We could actually reuse a template across a renderElement call? + if (templateData.renderedParts) { + try { + dispose(coalesce(templateData.renderedParts)); + templateData.renderedParts = undefined; + dom.clearNode(templateData.value); + } catch (err) { + throw err; + } + } + + templateData.currentElement = undefined; templateData.elementDisposables.clear(); } @@ -1280,469 +904,9 @@ export class ChatListDelegate implements IListVirtualDelegate { } } - -interface IDisposableReference extends IDisposable { - object: T; - isStale: () => boolean; -} - -class EditorPool extends Disposable { - - private readonly _pool: ResourcePool; - - public inUse(): Iterable { - return this._pool.inUse; - } - - constructor( - options: ChatEditorOptions, - delegate: IChatRendererDelegate, - overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => { - return instantiationService.createInstance(CodeBlockPart, options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode); - })); - } - - get(): IDisposableReference { - const codeBlock = this._pool.get(); - let stale = false; - return { - object: codeBlock, - isStale: () => stale, - dispose: () => { - codeBlock.reset(); - stale = true; - this._pool.release(codeBlock); - } - }; - } -} - -class DiffEditorPool extends Disposable { - - private readonly _pool: ResourcePool; - - public inUse(): Iterable { - return this._pool.inUse; - } - - constructor( - options: ChatEditorOptions, - delegate: IChatRendererDelegate, - overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => { - return instantiationService.createInstance(CodeCompareBlockPart, options, MenuId.ChatCompareBlock, delegate, overflowWidgetsDomNode); - })); - } - - get(): IDisposableReference { - const codeBlock = this._pool.get(); - let stale = false; - return { - object: codeBlock, - isStale: () => stale, - dispose: () => { - codeBlock.reset(); - stale = true; - this._pool.release(codeBlock); - } - }; - } -} - -class TreePool extends Disposable { - private _pool: ResourcePool>; - - public get inUse(): ReadonlySet> { - return this._pool.inUse; - } - - constructor( - private _onDidChangeVisibility: Event, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IConfigurationService private readonly configService: IConfigurationService, - @IThemeService private readonly themeService: IThemeService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => this.treeFactory())); - } - - private treeFactory(): WorkbenchCompressibleAsyncDataTree { - const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); - - const container = $('.interactive-response-progress-tree'); - this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); - - const tree = this.instantiationService.createInstance( - WorkbenchCompressibleAsyncDataTree, - 'ChatListRenderer', - container, - new ChatListTreeDelegate(), - new ChatListTreeCompressionDelegate(), - [new ChatListTreeRenderer(resourceLabels, this.configService.getValue('explorer.decorations'))], - new ChatListTreeDataSource(), - { - collapseByDefault: () => false, - expandOnlyOnTwistieClick: () => false, - identityProvider: { - getId: (e: IChatResponseProgressFileTreeData) => e.uri.toString() - }, - accessibilityProvider: { - getAriaLabel: (element: IChatResponseProgressFileTreeData) => element.label, - getWidgetAriaLabel: () => localize('treeAriaLabel', "File Tree") - }, - alwaysConsumeMouseWheel: false - }); - - return tree; - } - - get(): IDisposableReference> { - const object = this._pool.get(); - let stale = false; - return { - object, - isStale: () => stale, - dispose: () => { - stale = true; - this._pool.release(object); - } - }; - } -} - -class ContentReferencesListPool extends Disposable { - private _pool: ResourcePool>; - - public get inUse(): ReadonlySet> { - return this._pool.inUse; - } - - constructor( - private _onDidChangeVisibility: Event, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IThemeService private readonly themeService: IThemeService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => this.listFactory())); - } - - private listFactory(): WorkbenchList { - const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); - - const container = $('.chat-used-context-list'); - this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); - - const list = this.instantiationService.createInstance( - WorkbenchList, - 'ChatListRenderer', - container, - new ContentReferencesListDelegate(), - [this.instantiationService.createInstance(ContentReferencesListRenderer, resourceLabels)], - { - alwaysConsumeMouseWheel: false, - accessibilityProvider: { - getAriaLabel: (element: IChatContentReference | IChatWarningMessage) => { - if (element.kind === 'warning') { - return element.content.value; - } - const reference = element.reference; - if ('variableName' in reference) { - return reference.variableName; - } else if (URI.isUri(reference)) { - return basename(reference.path); - } else { - return basename(reference.uri.path); - } - }, - - getWidgetAriaLabel: () => localize('usedReferences', "Used References") - }, - dnd: { - getDragURI: (element: IChatContentReference | IChatWarningMessage) => { - if (element.kind === 'warning') { - return null; - } - const { reference } = element; - if ('variableName' in reference) { - return null; - } else if (URI.isUri(reference)) { - return reference.toString(); - } else { - return reference.uri.toString(); - } - }, - dispose: () => { }, - onDragOver: () => false, - drop: () => { }, - }, - }); - - return list; - } - - get(): IDisposableReference> { - const object = this._pool.get(); - let stale = false; - return { - object, - isStale: () => stale, - dispose: () => { - stale = true; - this._pool.release(object); - } - }; - } -} - -class ContentReferencesListDelegate implements IListVirtualDelegate { - getHeight(element: IChatContentReference): number { - return 22; - } - - getTemplateId(element: IChatContentReference): string { - return ContentReferencesListRenderer.TEMPLATE_ID; - } -} - -interface IChatContentReferenceListTemplate { - label: IResourceLabel; - templateDisposables: IDisposable; -} - -class ContentReferencesListRenderer implements IListRenderer { - static TEMPLATE_ID = 'contentReferencesListRenderer'; - readonly templateId: string = ContentReferencesListRenderer.TEMPLATE_ID; - - constructor( - private labels: ResourceLabels, - @IThemeService private readonly themeService: IThemeService, - @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, - ) { } - - renderTemplate(container: HTMLElement): IChatContentReferenceListTemplate { - const templateDisposables = new DisposableStore(); - const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); - return { templateDisposables, label }; - } - - - private getReferenceIcon(data: IChatContentReference): URI | ThemeIcon | undefined { - if (ThemeIcon.isThemeIcon(data.iconPath)) { - return data.iconPath; - } else { - return this.themeService.getColorTheme().type === ColorScheme.DARK && data.iconPath?.dark - ? data.iconPath?.dark - : data.iconPath?.light; - } - } - - renderElement(data: IChatContentReference | IChatWarningMessage, index: number, templateData: IChatContentReferenceListTemplate, height: number | undefined): void { - if (data.kind === 'warning') { - templateData.label.setResource({ name: data.content.value }, { icon: Codicon.warning }); - return; - } - - const reference = data.reference; - const icon = this.getReferenceIcon(data); - templateData.label.element.style.display = 'flex'; - if ('variableName' in reference) { - if (reference.value) { - const uri = URI.isUri(reference.value) ? reference.value : reference.value.uri; - templateData.label.setResource( - { - resource: uri, - name: basenameOrAuthority(uri), - description: `#${reference.variableName}`, - range: 'range' in reference.value ? reference.value.range : undefined, - }, { icon }); - } else { - const variable = this.chatVariablesService.getVariable(reference.variableName); - templateData.label.setLabel(`#${reference.variableName}`, undefined, { title: variable?.description }); - } - } else { - const uri = 'uri' in reference ? reference.uri : reference; - if (matchesSomeScheme(uri, Schemas.mailto, Schemas.http, Schemas.https)) { - templateData.label.setResource({ resource: uri, name: uri.toString() }, { icon: icon ?? Codicon.globe }); - } else { - templateData.label.setFile(uri, { - fileKind: FileKind.FILE, - // Should not have this live-updating data on a historical reference - fileDecorations: { badges: false, colors: false }, - range: 'range' in reference ? reference.range : undefined - }); - } - } - } - - disposeTemplate(templateData: IChatContentReferenceListTemplate): void { - templateData.templateDisposables.dispose(); - } -} - -class ResourcePool extends Disposable { - private readonly pool: T[] = []; - - private _inUse = new Set; - public get inUse(): ReadonlySet { - return this._inUse; - } - - constructor( - private readonly _itemFactory: () => T, - ) { - super(); - } - - get(): T { - if (this.pool.length > 0) { - const item = this.pool.pop()!; - this._inUse.add(item); - return item; - } - - const item = this._register(this._itemFactory()); - this._inUse.add(item); - return item; - } - - release(item: T): void { - this._inUse.delete(item); - this.pool.push(item); - } -} - class ChatVoteButton extends MenuEntryActionViewItem { override render(container: HTMLElement): void { super.render(container); container.classList.toggle('checked', this.action.checked); } } - -class ChatListTreeDelegate implements IListVirtualDelegate { - static readonly ITEM_HEIGHT = 22; - - getHeight(element: IChatResponseProgressFileTreeData): number { - return ChatListTreeDelegate.ITEM_HEIGHT; - } - - getTemplateId(element: IChatResponseProgressFileTreeData): string { - return 'chatListTreeTemplate'; - } -} - -class ChatListTreeCompressionDelegate implements ITreeCompressionDelegate { - isIncompressible(element: IChatResponseProgressFileTreeData): boolean { - return !element.children; - } -} - -interface IChatListTreeRendererTemplate { - templateDisposables: DisposableStore; - label: IResourceLabel; -} - -class ChatListTreeRenderer implements ICompressibleTreeRenderer { - templateId: string = 'chatListTreeTemplate'; - - constructor(private labels: ResourceLabels, private decorations: IFilesConfiguration['explorer']['decorations']) { } - - renderCompressedElements(element: ITreeNode, void>, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { - templateData.label.element.style.display = 'flex'; - const label = element.element.elements.map((e) => e.label); - templateData.label.setResource({ resource: element.element.elements[0].uri, name: label }, { - title: element.element.elements[0].label, - fileKind: element.children ? FileKind.FOLDER : FileKind.FILE, - extraClasses: ['explorer-item'], - fileDecorations: this.decorations - }); - } - renderTemplate(container: HTMLElement): IChatListTreeRendererTemplate { - const templateDisposables = new DisposableStore(); - const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); - return { templateDisposables, label }; - } - renderElement(element: ITreeNode, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { - templateData.label.element.style.display = 'flex'; - if (!element.children.length && element.element.type !== FileType.Directory) { - templateData.label.setFile(element.element.uri, { - fileKind: FileKind.FILE, - hidePath: true, - fileDecorations: this.decorations, - }); - } else { - templateData.label.setResource({ resource: element.element.uri, name: element.element.label }, { - title: element.element.label, - fileKind: FileKind.FOLDER, - fileDecorations: this.decorations - }); - } - } - disposeTemplate(templateData: IChatListTreeRendererTemplate): void { - templateData.templateDisposables.dispose(); - } -} - -class ChatListTreeDataSource implements IAsyncDataSource { - hasChildren(element: IChatResponseProgressFileTreeData): boolean { - return !!element.children; - } - - async getChildren(element: IChatResponseProgressFileTreeData): Promise> { - return element.children ?? []; - } -} - -function isInteractiveProgressTreeData(item: Object): item is IChatResponseProgressFileTreeData { - return 'label' in item; -} - -function contentToMarkdown(str: string | IMarkdownString): IMarkdownString { - return typeof str === 'string' ? { value: str } : str; -} - -function isProgressMessage(item: any): item is IChatProgressMessage { - return item && 'kind' in item && item.kind === 'progressMessage'; -} - -function isProgressTaskRenderData(item: any): item is IChatTaskRenderData { - return item && 'isSettled' in item; -} - -function isWarningRenderData(item: any): item is IChatWarningMessage { - return item && 'kind' in item && item.kind === 'warning'; -} - -function isProgressMessageRenderData(item: IChatRenderData): item is IChatProgressMessageRenderData { - return item && 'isAtEndOfResponse' in item; -} - -function isCommandButtonRenderData(item: IChatRenderData): item is IChatCommandButton { - return item && 'kind' in item && item.kind === 'command'; -} - -function isTextEditRenderData(item: IChatRenderData): item is IChatTextEditGroup { - return item && 'kind' in item && item.kind === 'textEditGroup'; -} - -function isConfirmationRenderData(item: IChatRenderData): item is IChatConfirmation { - return item && 'kind' in item && item.kind === 'confirmation'; -} - -function isMarkdownRenderData(item: IChatRenderData): item is IChatResponseMarkdownRenderData { - return item && 'renderedWordCount' in item; -} - -function onlyProgressMessagesAfterI(items: ReadonlyArray, i: number): boolean { - return items.slice(i).every(isProgressMessage); -} - -function onlyProgressMessageRenderDatasAfterI(items: ReadonlyArray, i: number): boolean { - return items.slice(i).every(isProgressMessageRenderData); -} diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 25c4ac91e44..9af542462c3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -42,7 +42,7 @@ export function agentToMarkdown(agent: IChatAgentData, isClickable: boolean, acc const isAllowed = chatAgentNameService.getAgentNameRestriction(agent); let name = `${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; - const isDupe = isAllowed && chatAgentService.getAgentsByName(agent.name).length > 1; + const isDupe = isAllowed && chatAgentService.agentHasDupeName(agent.id); if (isDupe) { name += ` (${agent.publisherDisplayName})`; } @@ -177,7 +177,7 @@ export class ChatMarkdownDecorationsRenderer { const agent = this.chatAgentService.getAgent(args.agentId); const hover: Lazy = new Lazy(() => store.add(this.instantiationService.createInstance(ChatAgentHover))); - store.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), container, () => { + store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), container, () => { hover.value.setAgent(args.agentId); return hover.value.domNode; }, agent && getChatAgentHoverOptions(() => agent, this.commandService))); diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts index 3e4b6a9349f..50c6029a9e8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts @@ -73,7 +73,8 @@ export class ChatMarkdownRenderer extends MarkdownRenderer { ...markdown, // dompurify uses DOMParser, which strips leading comments. Wrapping it all in 'body' prevents this. - value: `${markdown.value}`, + // The \n\n prevents marked.js from parsing the body contents as just text in an 'html' token, instead of actual markdown. + value: `\n\n${markdown.value}`, } : markdown; return super.render(mdWithBody, options, markedOptions); diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index 43eb5d2be7b..33c61973533 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -63,6 +63,10 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi description: localize('chatSampleRequest', "When the user clicks this participant in `/help`, this text will be submitted to the participant."), type: 'string' }, + when: { + description: localize('chatParticipantWhen', "A condition which must be true to enable this participant."), + type: 'string' + }, commands: { markdownDescription: localize('chatCommandsDescription', "Commands available for this chat participant, which the user can invoke with a `/`."), type: 'array', @@ -181,11 +185,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { continue; } - if (this.productService.quality === 'stable' && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { - this.logService.warn(`Chat participants are not yet enabled in VS Code Stable (${extension.description.identifier.value})`); - continue; - } - for (const providerDescriptor of extension.value) { if (!providerDescriptor.name.match(/^[\w0-9_-]+$/)) { this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with invalid name: ${providerDescriptor.name}. Name must match /^[\\w0-9_-]+$/.`); @@ -223,11 +222,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { store.add(this.registerDefaultParticipantView(providerDescriptor)); } - if (providerDescriptor.when && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) { - this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: chatParticipantAdditions.`); - continue; - } - store.add(this._chatAgentService.registerAgent( providerDescriptor.id, { diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 61b4ee1984f..cd978afa041 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -90,11 +90,15 @@ export class ChatVariablesService implements IChatVariablesService { await Promise.allSettled(jobs); + // Make array not sparse resolvedVariables = coalesce(resolvedVariables); // "reverse", high index first so that replacement is simple resolvedVariables.sort((a, b) => b.range!.start - a.range!.start); - resolvedVariables.push(...resolvedAttachedContext); + + // resolvedAttachedContext is a sparse array + resolvedVariables.push(...coalesce(resolvedAttachedContext)); + return { variables: resolvedVariables, diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 830664edef0..1d1dc4b817c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -69,6 +69,8 @@ export interface IChatWidgetContrib extends IDisposable { */ getInputState?(): any; + onDidChangeInputState?: Event; + /** * Called with the result of getInputState when navigating input history. */ @@ -105,13 +107,16 @@ export class ChatWidget extends Disposable implements IChatWidget { private _onDidChangeParsedInput = this._register(new Emitter()); readonly onDidChangeParsedInput = this._onDidChangeParsedInput.event; + private readonly _onWillMaybeChangeHeight = new Emitter(); + readonly onWillMaybeChangeHeight: Event = this._onWillMaybeChangeHeight.event; + private _onDidChangeHeight = this._register(new Emitter()); readonly onDidChangeHeight = this._onDidChangeHeight.event; private readonly _onDidChangeContentHeight = new Emitter(); readonly onDidChangeContentHeight: Event = this._onDidChangeContentHeight.event; - private contribs: IChatWidgetContrib[] = []; + private contribs: ReadonlyArray = []; private tree!: WorkbenchObjectTree; private renderer!: ChatListItemRenderer; @@ -311,6 +316,15 @@ export class ChatWidget extends Disposable implements IChatWidget { return undefined; } }).filter(isDefined); + + this.contribs.forEach(c => { + if (c.onDidChangeInputState) { + this._register(c.onDidChangeInputState(() => { + const state = this.collectInputState(); + this.inputPart.updateState(state); + })); + } + }); } getContrib(id: string): T | undefined { @@ -363,6 +377,8 @@ export class ChatWidget extends Disposable implements IChatWidget { }; }); + this._onWillMaybeChangeHeight.fire(); + this.tree.setChildren(null, treeItems, { diffIdentityProvider: { getId: (element) => { @@ -538,12 +554,12 @@ export class ChatWidget extends Disposable implements IChatWidget { this._onDidChangeContentHeight.fire(); } - private createInput(container: HTMLElement, options?: { renderFollowups: boolean; renderStyle?: 'default' | 'compact' }): void { + private createInput(container: HTMLElement, options?: { renderFollowups: boolean; renderStyle?: 'default' | 'compact' | 'minimal' }): void { this.inputPart = this._register(this.instantiationService.createInstance(ChatInputPart, this.location, { renderFollowups: options?.renderFollowups ?? true, - renderStyle: options?.renderStyle, + renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle, menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus }, editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode, } @@ -552,8 +568,9 @@ export class ChatWidget extends Disposable implements IChatWidget { this._register(this.inputPart.onDidLoadInputState(state => { this.contribs.forEach(c => { - if (c.setInputState && typeof state === 'object' && state?.[c.id]) { - c.setInputState(state[c.id]); + if (c.setInputState) { + const contribState = (typeof state === 'object' && state?.[c.id]) ?? {}; + c.setInputState(contribState); } }); })); @@ -646,7 +663,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.viewModel = undefined; this.onDidChangeItems(); })); - this.inputPart.setState(viewState.inputValue); + this.inputPart.initForNewChatModel(viewState.inputValue, viewState.inputState ?? this.collectInputState()); this.contribs.forEach(c => { if (c.setInputState && viewState.inputState?.[c.id]) { c.setInputState(viewState.inputState?.[c.id]); @@ -692,13 +709,17 @@ export class ChatWidget extends Disposable implements IChatWidget { } setInput(value = ''): void { - this.inputPart.setValue(value); + this.inputPart.setValue(value, false); } getInput(): string { return this.inputPart.inputEditor.getValue(); } + logInputHistory(): void { + this.inputPart.logInputHistory(); + } + async acceptInput(query?: string): Promise { return this._acceptInput(query ? { query } : undefined); } @@ -731,9 +752,9 @@ export class ChatWidget extends Disposable implements IChatWidget { if (result) { this.inputPart.attachedContext.clear(); - const inputState = this.collectInputState(); - this.inputPart.acceptInput(isUserQuery ? input : undefined, isUserQuery ? inputState : undefined); + this.inputPart.acceptInput(isUserQuery); this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand }); + this.inputPart.updateState(this.collectInputState()); result.responseCompletePromise.then(() => { const responses = this.viewModel?.getItems().filter(isResponseVM); const lastResponse = responses?.[responses.length - 1]; diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.css b/src/vs/workbench/contrib/chat/browser/codeBlockPart.css index 279626daf82..89596f89748 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.css +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.css @@ -147,3 +147,17 @@ .interactive-result-code-block.compare .message A > CODE { color: var(--vscode-textLink-foreground); } + +.interactive-result-code-block.compare .interactive-result-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 3px; + box-sizing: border-box; + border-bottom: solid 1px var(--vscode-chat-requestBorder); +} + +.interactive-result-code-block.compare.no-diff .interactive-result-header, +.interactive-result-code-block.compare.no-diff .interactive-result-editor { + display: none; +} diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 46e306fd9d2..c9e952ffcd0 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -6,24 +6,37 @@ import 'vs/css!./codeBlockPart'; import * as dom from 'vs/base/browser/dom'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { Button } from 'vs/base/browser/ui/button/button'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; +import { TabFocus } from 'vs/editor/browser/config/tabFocus'; +import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { EDITOR_FONT_DEFAULTS, EditorOption, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; import { IDiffEditorViewModel, ScrollType } from 'vs/editor/common/editorCommon'; +import { TextEdit } from 'vs/editor/common/languages'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; +import { TextModelText } from 'vs/editor/common/model/textModelText'; import { IModelService } from 'vs/editor/common/services/model'; +import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { BracketMatchingController } from 'vs/editor/contrib/bracketMatching/browser/bracketMatching'; +import { ColorDetector } from 'vs/editor/contrib/colorPicker/browser/colorDetector'; import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import { ViewportSemanticTokensContribution } from 'vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens'; import { SmartSelectController } from 'vs/editor/contrib/smartSelect/browser/smartSelect'; import { WordHighlighterContribution } from 'vs/editor/contrib/wordHighlighter/browser/wordHighlighter'; @@ -33,36 +46,24 @@ import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; +import { CONTEXT_CHAT_EDIT_APPLIED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatResponseModel, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { IMarkdownVulnerability } from '../common/annotations'; -import { TabFocus } from 'vs/editor/browser/config/tabFocus'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; -import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; -import { CONTEXT_CHAT_EDIT_APPLIED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IChatResponseModel, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; -import { TextEdit } from 'vs/editor/common/languages'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { basename, isEqual } from 'vs/base/common/resources'; -import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { TextModelText } from 'vs/editor/common/model/textModelText'; -import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { toAction } from 'vs/base/common/actions'; +import { ResourceLabel } from 'vs/workbench/browser/labels'; +import { FileKind } from 'vs/platform/files/common/files'; const $ = dom.$; @@ -143,6 +144,7 @@ export class CodeBlockPart extends Disposable { private currentScrollWidth = 0; private readonly disposableStore = this._register(new DisposableStore()); + private isDisposed = false; constructor( private readonly options: ChatEditorOptions, @@ -263,6 +265,11 @@ export class CodeBlockPart extends Disposable { } } + override dispose() { + this.isDisposed = true; + super.dispose(); + } + get uri(): URI | undefined { return this.editor.getModel()?.uri; } @@ -282,6 +289,7 @@ export class CodeBlockPart extends Disposable { HoverController.ID, MessageController.ID, GotoDefinitionAtPositionEditorContribution.ID, + ColorDetector.ID ]) })); } @@ -354,6 +362,9 @@ export class CodeBlockPart extends Disposable { } await this.updateEditor(data); + if (this.isDisposed) { + return; + } this.layout(width); if (editable) { @@ -458,7 +469,7 @@ export interface ICodeCompareBlockData { readonly diffData: Promise; readonly parentContextKeyService?: IContextKeyService; - readonly hideToolbar?: boolean; + // readonly hideToolbar?: boolean; } @@ -468,8 +479,8 @@ export class CodeCompareBlockPart extends Disposable { private readonly contextKeyService: IContextKeyService; private readonly diffEditor: DiffEditorWidget; - private readonly toolbar1: ActionBar; - private readonly toolbar2: MenuWorkbenchToolBar; + private readonly resourceLabel: ResourceLabel; + private readonly toolbar: MenuWorkbenchToolBar; readonly element: HTMLElement; private readonly messageElement: HTMLElement; @@ -499,6 +510,7 @@ export class CodeCompareBlockPart extends Disposable { this.contextKeyService = this._register(contextKeyService.createScoped(this.element)); const scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])); + const editorHeader = dom.append(this.element, $('.interactive-result-header.show-file-icons')); const editorElement = dom.append(this.element, $('.interactive-result-editor')); this.diffEditor = this.createDiffEditor(scopedInstantiationService, editorElement, { ...getSimpleEditorOptions(this.configurationService), @@ -525,24 +537,16 @@ export class CodeCompareBlockPart extends Disposable { ...this.getEditorOptionsFromConfig(), }); - const toolbarElement = dom.append(this.element, $('.interactive-result-code-block-toolbar')); - - // this.resourceLabel = this._register(scopedInstantiationService.createInstance(ResourceLabel, toolbarElement, { supportIcons: true })); + this.resourceLabel = this._register(scopedInstantiationService.createInstance(ResourceLabel, editorHeader, { supportIcons: true })); - const editorScopedService = this.diffEditor.getModifiedEditor().contextKeyService.createScoped(toolbarElement); + const editorScopedService = this.diffEditor.getModifiedEditor().contextKeyService.createScoped(editorHeader); const editorScopedInstantiationService = scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService])); - this.toolbar1 = this._register(new ActionBar(toolbarElement, {})); - this.toolbar2 = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, toolbarElement, menuId, { + this.toolbar = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, editorHeader, menuId, { menuOptions: { shouldForwardArgs: true } })); - - this._register(this.toolbar2.onDidChangeDropdownVisibility(e => { - toolbarElement.classList.toggle('force-visibility', e); - })); - this._configureForScreenReader(); this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this._configureForScreenReader())); this._register(this.configurationService.onDidChangeConfiguration((e) => { @@ -637,7 +641,7 @@ export class CodeCompareBlockPart extends Disposable { } private _configureForScreenReader(): void { - const toolbarElt = this.toolbar2.getElement(); + const toolbarElt = this.toolbar.getElement(); if (this.accessibilityService.isScreenReaderOptimized()) { toolbarElt.style.display = 'block'; toolbarElt.ariaLabel = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat) ? localize('chat.codeBlock.toolbarVerbose', 'Toolbar for code block which can be reached via tab') : localize('chat.codeBlock.toolbar', 'Code block toolbar'); @@ -690,21 +694,10 @@ export class CodeCompareBlockPart extends Disposable { this.layout(width); this.diffEditor.updateOptions({ ariaLabel: localize('chat.compareCodeBlockLabel', "Code Edits") }); - this.toolbar1.clear(); - this.toolbar1.push(toAction({ - label: basename(data.edit.uri), - tooltip: localize('chat.edit.tooltip', "Open '{0}'", this.labelService.getUriLabel(data.edit.uri, { relative: true })), - run: () => { - this.openerService.open(data.edit.uri, { fromUserGesture: true, allowCommands: false }); - }, - id: '', - }), { icon: false, label: true }); - - if (data.hideToolbar) { - dom.hide(this.toolbar2.getElement()); - } else { - dom.show(this.toolbar2.getElement()); - } + this.resourceLabel.element.setFile(data.edit.uri, { + fileKind: FileKind.FILE, + fileDecorations: { colors: true, badges: false } + }); } reset() { @@ -732,10 +725,14 @@ export class CodeCompareBlockPart extends Disposable { const uriLabel = this.labelService.getUriLabel(data.edit.uri, { relative: true, noPrefix: true }); - const template = data.edit.state.applied > 1 - ? localize('chat.edits.N', "Made {0} changes in [[``{1}``]]", data.edit.state.applied, uriLabel) - : localize('chat.edits.1', "Made 1 change in [[``{0}``]]", uriLabel); - + let template: string; + if (data.edit.state.applied === 1) { + template = localize('chat.edits.1', "Made 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); + } const message = renderFormattedText(template, { renderCodeSegments: true, @@ -776,7 +773,7 @@ export class CodeCompareBlockPart extends Disposable { this._lastDiffEditorViewModel.value = undefined; } - this.toolbar2.context = { + this.toolbar.context = { edit: data.edit, element: data.element, diffEditor: this.diffEditor, @@ -888,4 +885,18 @@ export class DefaultChatTextEditor { } return true; } + + discard(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup) { + if (!response.response.value.includes(item)) { + // bogous item + return; + } + + if (item.state?.applied) { + // already applied + return; + } + + response.setEditApplied(item, -1); + } } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts index ff0a4455486..c15a1f6aee3 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatWidget, IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; @@ -12,6 +13,9 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon private _attachedContext = new Set(); + private readonly _onDidChangeInputState = this._register(new Emitter()); + readonly onDidChangeInputState = this._onDidChangeInputState.event; + public static readonly ID = 'chatContextAttachments'; get id() { @@ -30,13 +34,18 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon })); } - getInputState?() { + getInputState(): IChatRequestVariableEntry[] { return [...this._attachedContext.values()]; } - setInputState?(s: any): void { + setInputState(s: any): void { if (!Array.isArray(s)) { - return; + s = []; + } + + this._attachedContext.clear(); + for (const attachment of s) { + this._attachedContext.add(attachment); } this.widget.setContext(true, ...s); @@ -55,10 +64,12 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon } this.widget.setContext(overwrite, ...attachments); + this._onDidChangeInputState.fire(); } private _removeContext(attachment: IChatRequestVariableEntry) { this._attachedContext.delete(attachment); + this._onDidChangeInputState.fire(); } private _clearAttachedContext() { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 4c3fa0ee29c..2d8bf253fd0 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce } from 'vs/base/common/arrays'; +import { Emitter } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/resources'; @@ -38,24 +39,30 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC return ChatDynamicVariableModel.ID; } + private _onDidChangeInputState = this._register(new Emitter()); + readonly onDidChangeInputState = this._onDidChangeInputState.event; + constructor( private readonly widget: IChatWidget, @ILabelService private readonly labelService: ILabelService, - @ILogService private readonly logService: ILogService, ) { super(); this._register(widget.inputEditor.onDidChangeModelContent(e => { e.changes.forEach(c => { // Don't mutate entries in _variables, since they will be returned from the getter + const originalNumVariables = this._variables.length; this._variables = coalesce(this._variables.map(ref => { const intersection = Range.intersectRanges(ref.range, c.range); if (intersection && !intersection.isEmpty()) { - // The reference text was changed, it's broken - const rangeToDelete = new Range(ref.range.startLineNumber, ref.range.startColumn, ref.range.endLineNumber, ref.range.endColumn - 1); - this.widget.inputEditor.executeEdits(this.id, [{ - range: rangeToDelete, - text: '', - }]); + // The reference text was changed, it's broken. + // But if the whole reference range was deleted (eg history navigation) then don't try to change the editor. + if (!Range.containsRange(c.range, ref.range)) { + const rangeToDelete = new Range(ref.range.startLineNumber, ref.range.startColumn, ref.range.endLineNumber, ref.range.endColumn - 1); + this.widget.inputEditor.executeEdits(this.id, [{ + range: rangeToDelete, + text: '', + }]); + } return null; } else if (Range.compareRangesUsingStarts(ref.range, c.range) > 0) { const delta = c.text.length - c.rangeLength; @@ -72,6 +79,10 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC return ref; })); + + if (this._variables.length !== originalNumVariables) { + this._onDidChangeInputState.fire(); + } }); this.updateDecorations(); @@ -84,9 +95,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC setInputState(s: any): void { if (!Array.isArray(s)) { - // Something went wrong - this.logService.warn('ChatDynamicVariableModel.setInputState called with invalid state: ' + JSON.stringify(s)); - return; + s = []; } this._variables = s; @@ -96,6 +105,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC addReference(ref: IDynamicVariable): void { this._variables.push(ref); this.updateDecorations(); + this._onDidChangeInputState.fire(); } private updateDecorations(): void { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 647ac002a13..9509e38f8d1 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -13,6 +13,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { localize } from 'vs/nls'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; @@ -117,7 +118,7 @@ class AgentCompletions extends Disposable { return { suggestions: agents.map((agent, i): CompletionItem => { - const { label: agentLabel, isDupe } = getAgentCompletionDetails(agent, agents, this.chatAgentNameService); + const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); return { // Leading space is important because detail has no space at the start by design label: isDupe ? @@ -209,14 +210,14 @@ class AgentCompletions extends Disposable { const getFilterText = (agent: IChatAgentData, command: string) => { // This is hacking the filter algorithm to make @terminal /explain match worse than @workspace /explain by making its match index later in the string. // When I type `/exp`, the workspace one should be sorted over the terminal one. - const dummyPrefix = agent.id === 'github.copilot.terminal' ? `0000` : ``; + const dummyPrefix = agent.id === 'github.copilot.terminalPanel' ? `0000` : ``; return `${chatSubcommandLeader}${dummyPrefix}${agent.name}.${command}`; }; const justAgents: CompletionItem[] = agents .filter(a => !a.isDefault) .map(agent => { - const { label: agentLabel, isDupe } = getAgentCompletionDetails(agent, agents, this.chatAgentNameService); + const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); const detail = agent.description; return { @@ -236,7 +237,7 @@ class AgentCompletions extends Disposable { return { suggestions: justAgents.concat( agents.flatMap(agent => agent.slashCommands.map((c, i) => { - const { label: agentLabel, isDupe } = getAgentCompletionDetails(agent, agents, this.chatAgentNameService); + const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); const withSlash = `${chatSubcommandLeader}${c.name}`; return { label: { label: withSlash, description: agentLabel, detail: isDupe ? ` (${agent.publisherDisplayName})` : undefined }, @@ -254,6 +255,13 @@ class AgentCompletions extends Disposable { } })); } + + private getAgentCompletionDetails(agent: IChatAgentData): { label: string; isDupe: boolean } { + const isAllowed = this.chatAgentNameService.getAgentNameRestriction(agent); + const agentLabel = `${chatAgentLeader}${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; + const isDupe = isAllowed && this.chatAgentService.agentHasDupeName(agent.id); + return { label: agentLabel, isDupe }; + } } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually); @@ -354,6 +362,7 @@ class VariableCompletions extends Disposable { @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, + @IConfigurationService configService: IConfigurationService, ) { super(); @@ -362,8 +371,17 @@ class VariableCompletions extends Disposable { triggerCharacters: [chatVariableLeader], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + const locations = new Set(); + locations.add(ChatAgentLocation.Panel); + + for (const value of Object.values(ChatAgentLocation)) { + if (typeof value === 'string' && configService.getValue(`chat.experimental.variables.${value}`)) { + locations.add(value); + } + } + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + if (!widget || !locations.has(widget.location)) { return null; } @@ -401,11 +419,3 @@ class VariableCompletions extends Disposable { } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(VariableCompletions, LifecyclePhase.Eventually); - -function getAgentCompletionDetails(agent: IChatAgentData, otherAgents: IChatAgentData[], chatAgentNameService: IChatAgentNameService): { label: string; isDupe: boolean } { - const isAllowed = chatAgentNameService.getAgentNameRestriction(agent); - const agentLabel = `${chatAgentLeader}${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; - const isDupe = isAllowed && !!otherAgents.find(other => other.name === agent.name && other.id !== agent.id); - - return { label: agentLabel, isDupe }; -} diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index bafc22d77ee..e58d0fd8294 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -197,10 +197,7 @@ class InputEditorDecorations extends Disposable { const textDecorations: IDecorationOptions[] | undefined = []; if (agentPart) { - const isDupe = !!this.chatAgentService.getAgents().find(other => other.name === agentPart.agent.name && other.id !== agentPart.agent.id); - const publisher = isDupe ? `(${agentPart.agent.publisherDisplayName}) ` : ''; - const agentHover = `${publisher}${agentPart.agent.description}`; - textDecorations.push({ range: agentPart.editorRange, hoverMessage: new MarkdownString(agentHover) }); + textDecorations.push({ range: agentPart.editorRange }); if (agentSubcommandPart) { textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) }); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts new file mode 100644 index 00000000000..893353b52f1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { ChatEditorHoverWrapper } from 'vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper'; +import { IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { extractAgentAndCommand } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import * as nls from 'vs/nls'; + +export class ChatAgentHoverParticipant implements IEditorHoverParticipant { + + public readonly hoverOrdinal: number = 1; + + constructor( + private readonly editor: ICodeEditor, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @ICommandService private readonly commandService: ICommandService, + ) { } + + public computeSync(anchor: HoverAnchor, _lineDecorations: IModelDecoration[]): ChatAgentHoverPart[] { + if (!this.editor.hasModel()) { + return []; + } + + const widget = this.chatWidgetService.getWidgetByInputUri(this.editor.getModel().uri); + if (!widget) { + return []; + } + + const { agentPart } = extractAgentAndCommand(widget.parsedInput); + if (!agentPart) { + return []; + } + + if (Range.containsPosition(agentPart.editorRange, anchor.range.getStartPosition())) { + return [new ChatAgentHoverPart(this, Range.lift(agentPart.editorRange), agentPart.agent)]; + } + + return []; + } + + public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: ChatAgentHoverPart[]): IRenderedHoverParts { + if (!hoverParts.length) { + return new RenderedHoverParts([]); + } + + const disposables = new DisposableStore(); + const hover = disposables.add(this.instantiationService.createInstance(ChatAgentHover)); + disposables.add(hover.onDidChangeContents(() => context.onContentsChanged())); + const hoverPart = hoverParts[0]; + const agent = hoverPart.agent; + hover.setAgent(agent.id); + + const actions = getChatAgentHoverOptions(() => agent, this.commandService).actions; + const wrapper = this.instantiationService.createInstance(ChatEditorHoverWrapper, hover.domNode, actions); + const wrapperNode = wrapper.domNode; + context.fragment.appendChild(wrapperNode); + const renderedHoverPart: IRenderedHoverPart = { + hoverPart, + hoverElement: wrapperNode, + dispose() { disposables.dispose(); } + }; + return new RenderedHoverParts([renderedHoverPart]); + } + + public getAccessibleContent(hoverPart: ChatAgentHoverPart): string { + return nls.localize('hoverAccessibilityChatAgent', 'There is a chat agent hover part here.'); + + } +} + +export class ChatAgentHoverPart implements IHoverPart { + + constructor( + public readonly owner: IEditorHoverParticipant, + public readonly range: Range, + public readonly agent: IChatAgentData + ) { } + + public isValidForHoverAnchor(anchor: HoverAnchor): boolean { + return ( + anchor.type === HoverAnchorType.Range + && this.range.startColumn <= anchor.range.startColumn + && this.range.endColumn >= anchor.range.endColumn + ); + } +} + +HoverParticipantRegistry.register(ChatAgentHoverParticipant); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts b/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts new file mode 100644 index 00000000000..5cbc7d932cc --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.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 'vs/css!./media/editorHoverWrapper'; +import * as dom from 'vs/base/browser/dom'; +import { IHoverAction } from 'vs/base/browser/ui/hover/hover'; +import { HoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; + +const $ = dom.$; +const h = dom.h; + +/** + * This borrows some of HoverWidget so that a chat editor hover can be rendered in the same way as a workbench hover. + * Maybe it can be reusable in a generic way. + */ +export class ChatEditorHoverWrapper { + public readonly domNode: HTMLElement; + + constructor( + hoverContentElement: HTMLElement, + actions: IHoverAction[] | undefined, + @IKeybindingService private readonly keybindingService: IKeybindingService, + ) { + const hoverElement = h( + '.chat-editor-hover-wrapper@root', + [h('.chat-editor-hover-wrapper-content@content')]); + this.domNode = hoverElement.root; + hoverElement.content.appendChild(hoverContentElement); + + if (actions && actions.length > 0) { + const statusBarElement = $('.hover-row.status-bar'); + const actionsElement = $('.actions'); + actions.forEach(action => { + const keybinding = this.keybindingService.lookupKeybinding(action.commandId); + const keybindingLabel = keybinding ? keybinding.getLabel() : null; + HoverAction.render(actionsElement, { + label: action.label, + commandId: action.commandId, + run: e => { + action.run(e); + }, + iconClass: action.iconClass + }, keybindingLabel); + }); + statusBarElement.appendChild(actionsElement); + this.domNode.appendChild(statusBarElement); + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css b/src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css new file mode 100644 index 00000000000..d95fd395255 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-editor-hover-wrapper-content { + padding: 2px 8px; +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index c54f006ab9b..ec0866795d5 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -40,6 +40,7 @@ display: flex; align-items: center; gap: 8px; + width: 100%; } .interactive-item-container .header .username { @@ -48,12 +49,13 @@ font-weight: 600; } -.interactive-item-container .header .detail-container { +.interactive-item-container .detail-container { font-size: 12px; color: var(--vscode-descriptionForeground); + overflow: hidden; } -.interactive-item-container .header .detail-container .detail .agentOrSlashCommandDetected A { +.interactive-item-container .detail-container .detail .agentOrSlashCommandDetected A { cursor: pointer; color: var(--vscode-textLink-foreground); } @@ -170,8 +172,8 @@ width: 100%; } -.interactive-item-container .chat-progress-task { - padding-bottom: 8px; +.interactive-item-container > .value .chat-used-context { + margin-bottom: 8px; } .interactive-item-container .value .rendered-markdown table { @@ -340,6 +342,34 @@ margin: 8px 0; } +.interactive-item-container.minimal { + flex-direction: row; +} + +.interactive-item-container.minimal .column.left { + padding-top: 2px; + display: inline-block; + flex-grow: 0; +} + +.interactive-item-container.minimal .column.right { + display: inline-block; + flex-grow: 1; +} + +.interactive-item-container.minimal .user > .username { + display: none; +} + +.interactive-item-container.minimal .detail-container { + font-size: unset; +} + +.interactive-item-container.minimal > .header { + position: absolute; + right: 0; +} + .interactive-session .interactive-input-and-execute-toolbar { display: flex; box-sizing: border-box; @@ -554,6 +584,12 @@ display: block; color: var(--vscode-textLink-foreground); font-size: 12px; + + /* clamp to max 3 lines */ + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; } .interactive-session .interactive-input-part .interactive-input-followups .interactive-session-followups code { diff --git a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css index 1599c4ffeac..29e38f48cad 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css @@ -22,8 +22,8 @@ outline: 1px solid var(--vscode-chat-requestBorder); } -.monaco-hover .markdown-hover .hover-contents .chat-agent-hover-icon .codicon { - font-size: 23px; +.chat-agent-hover .chat-agent-hover-icon .codicon { + font-size: 23px !important; /* Override workbench hover styles */ display: flex; justify-content: center; align-items: center; @@ -34,7 +34,7 @@ gap: 4px; } -.monaco-hover .chat-agent-hover .chat-agent-hover-publisher .codicon.codicon-extensions-verified-publisher { +.chat-agent-hover .chat-agent-hover-publisher .codicon.codicon-extensions-verified-publisher { color: var(--vscode-extensionIcon-verifiedForeground); } @@ -60,6 +60,10 @@ font-weight: 600; } +.chat-agent-hover-header .chat-agent-hover-details { + font-size: 12px; +} + .chat-agent-hover-extension { display: flex; gap: 6px; diff --git a/src/vs/workbench/contrib/chat/common/annotations.ts b/src/vs/workbench/contrib/chat/common/annotations.ts index 8a57732c95d..449ff1bc2dd 100644 --- a/src/vs/workbench/contrib/chat/common/annotations.ts +++ b/src/vs/workbench/contrib/chat/common/annotations.ts @@ -11,7 +11,7 @@ import { IChatAgentVulnerabilityDetails, IChatMarkdownContent } from 'vs/workben export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI -export function annotateSpecialMarkdownContent(response: ReadonlyArray): ReadonlyArray { +export function annotateSpecialMarkdownContent(response: ReadonlyArray): IChatProgressRenderableResponseContent[] { const result: IChatProgressRenderableResponseContent[] = []; for (const item of response) { const previousItem = result[result.length - 1]; diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index da0e5eba9d9..e2f703325b1 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -174,6 +174,7 @@ export interface IChatAgentService { getAgents(): IChatAgentData[]; getActivatedAgents(): Array; getAgentsByName(name: string): IChatAgentData[]; + agentHasDupeName(id: string): boolean; /** * Get the default agent (only if activated) @@ -345,6 +346,16 @@ export class ChatAgentService implements IChatAgentService { return this.getAgents().filter(a => a.name === name); } + agentHasDupeName(id: string): boolean { + const agent = this.getAgent(id); + if (!agent) { + return false; + } + + return this.getAgentsByName(agent.name) + .filter(a => a.extensionId.value !== agent.extensionId.value).length > 0; + } + async invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._agents.get(id); if (!data?.impl) { diff --git a/src/vs/workbench/contrib/chat/common/chatColors.ts b/src/vs/workbench/contrib/chat/common/chatColors.ts index 3c4cce05e01..15451f0de58 100644 --- a/src/vs/workbench/contrib/chat/common/chatColors.ts +++ b/src/vs/workbench/contrib/chat/common/chatColors.ts @@ -39,6 +39,6 @@ export const chatAvatarBackground = registerColor( export const chatAvatarForeground = registerColor( 'chat.avatarForeground', - { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground, }, + foreground, localize('chat.avatarForeground', 'The foreground color of a chat avatar.') ); diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 1760a265c3b..3e71298a2cb 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -232,7 +232,7 @@ export class Response implements IResponse { // Replace the resolving part's content with the resolved response if (typeof content === 'string') { - this._responseParts[responsePosition] = { ...progress, content: new MarkdownString(content) }; + (this._responseParts[responsePosition] as IChatTask).content = new MarkdownString(content); } this._updateRepr(false); }); @@ -725,7 +725,8 @@ export class ChatModel extends Disposable implements IChatModel { { variables: [] }; variableData.variables = variableData.variables.map((v): IChatRequestVariableEntry => { - if ('values' in v && Array.isArray(v.values)) { + // Old variables format + if (v && 'values' in v && Array.isArray(v.values)) { return { id: v.id ?? '', name: v.name, diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 1ab8c2a4998..ff8d83fc2c3 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -611,8 +611,8 @@ export class ChatService extends Disposable implements IChatService { if (!request.response) { continue; } - history.push({ role: ChatMessageRole.User, content: request.message.text }); - history.push({ role: ChatMessageRole.Assistant, content: request.response.response.asString() }); + history.push({ role: ChatMessageRole.User, content: { type: 'text', value: request.message.text } }); + history.push({ role: ChatMessageRole.Assistant, content: { type: 'text', value: request.response.response.asString() } }); } const message = parsedRequest.text; const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress(p => { @@ -670,13 +670,13 @@ export class ChatService extends Disposable implements IChatService { chatSessionId: model.sessionId, location }); - const rawResult: IChatAgentResult = { errorDetails: { message: err.message } }; + this.logService.error(`Error while handling chat request: ${toErrorMessage(err, true)}`); if (request) { + const rawResult: IChatAgentResult = { errorDetails: { message: err.message } }; model.setResponse(request, rawResult); + completeResponseCreated(); + model.completeResponse(request); } - completeResponseCreated(); - this.logService.error(`Error while handling chat request: ${toErrorMessage(err)}`); - model.completeResponse(request); } finally { listener.dispose(); } diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 130ef76979b..bde3c18d399 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -13,9 +13,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILogService } from 'vs/platform/log/common/log'; import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/annotations'; import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModelInitState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatAgentVoteDirection, IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTask, IChatUsedContext, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { CodeBlockModelCollection } from './codeBlockModelCollection'; @@ -78,6 +78,13 @@ export interface IChatResponseMarkdownRenderData { originalMarkdown: IMarkdownString; } +export interface IChatResponseMarkdownRenderData2 { + renderedWordCount: number; + lastRenderTime: number; + isFullyRendered: boolean; + originalMarkdown: IMarkdownString; +} + export interface IChatProgressMessageRenderData { progressMessage: IChatProgressMessage; @@ -101,11 +108,26 @@ export interface IChatTaskRenderData { progressLength: number; } -export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup | IChatConfirmation | IChatTaskRenderData | IChatWarningMessage; export interface IChatResponseRenderData { - renderedParts: IChatRenderData[]; + renderedParts: IChatRendererContent[]; + + renderedWordCount: number; + lastRenderTime: number; } +/** + * Content type for references used during rendering, not in the model + */ +export interface IChatReferences { + references: ReadonlyArray; + kind: 'references'; +} + +/** + * Type for content parts rendered by IChatListRenderer + */ +export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences; + export interface IChatLiveUpdateData { loadingStartTime: number; lastUpdateTime: number; diff --git a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts b/src/vs/workbench/contrib/chat/common/chatWordCounter.ts index 94870296160..edd27ddc435 100644 --- a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts +++ b/src/vs/workbench/contrib/chat/common/chatWordCounter.ts @@ -5,13 +5,14 @@ export interface IWordCountResult { value: string; - actualWordCount: number; + returnedWordCount: number; + totalWordCount: number; isFullString: boolean; } export function getNWords(str: string, numWordsToCount: number): IWordCountResult { // Match words and markdown style links - const allWordMatches = Array.from(str.matchAll(/\[([^\]]+)\]\(([^)]+)\)|[^\s\|\-]+/g)); + const allWordMatches = Array.from(str.matchAll(/\[([^\]]+)\]\(([^)]+)\)|\p{sc=Han}|[^\s\|\-|\p{sc=Han}]+/gu)); const targetWords = allWordMatches.slice(0, numWordsToCount); @@ -22,12 +23,13 @@ export function getNWords(str: string, numWordsToCount: number): IWordCountResul const value = str.substring(0, endIndex); return { value, - actualWordCount: targetWords.length === 0 ? (value.length ? 1 : 0) : targetWords.length, - isFullString: endIndex >= str.length + returnedWordCount: targetWords.length === 0 ? (value.length ? 1 : 0) : targetWords.length, + isFullString: endIndex >= str.length, + totalWordCount: allWordMatches.length }; } export function countWords(str: string): number { const result = getNWords(str, Number.MAX_SAFE_INTEGER); - return result.actualWordCount; + return result.returnedWordCount; } diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index a857b64cb1d..d5ed699cd9b 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -22,6 +22,13 @@ export class CodeBlockModelCollection extends Disposable { vulns: readonly IMarkdownVulnerability[]; }>(); + /** + * Max number of models to keep in memory. + * + * Currently always maintains the most recently created models. + */ + private readonly maxModelCount = 100; + constructor( @ILanguageService private readonly languageService: ILanguageService, @ITextModelService private readonly textModelService: ITextModelService @@ -52,9 +59,28 @@ export class CodeBlockModelCollection extends Disposable { const uri = this.getUri(sessionId, chat, codeBlockIndex); const ref = this.textModelService.createModelReference(uri); this._models.set(uri, { model: ref, vulns: [] }); + + while (this._models.size > this.maxModelCount) { + const first = Array.from(this._models.keys()).at(0); + if (!first) { + break; + } + this.delete(first); + } + return { model: ref.then(ref => ref.object), vulns: [] }; } + private delete(codeBlockUri: URI) { + const entry = this._models.get(codeBlockUri); + if (!entry) { + return; + } + + entry.model.then(ref => ref.dispose()); + this._models.delete(codeBlockUri); + } + clear(): void { this._models.forEach(async entry => (await entry.model).dispose()); this._models.clear(); diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts new file mode 100644 index 00000000000..a1c70e6d1a4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/event'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Iterable } from 'vs/base/common/iterator'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +export interface IToolData { + name: string; + displayName?: string; + description: string; + parametersSchema?: Object; +} + +interface IToolEntry { + data: IToolData; + impl?: IToolImpl; +} + +export interface IToolImpl { + invoke(parameters: any, token: CancellationToken): Promise; +} + +export const ILanguageModelToolsService = createDecorator('ILanguageModelToolsService'); + +export interface IToolDelta { + added?: IToolData; + removed?: string; +} + +export interface ILanguageModelToolsService { + _serviceBrand: undefined; + onDidChangeTools: Event; + registerToolData(toolData: IToolData): IDisposable; + registerToolImplementation(name: string, tool: IToolImpl): IDisposable; + getTools(): Iterable>; + invokeTool(name: string, parameters: any, token: CancellationToken): Promise; +} + +export class LanguageModelToolsService implements ILanguageModelToolsService { + _serviceBrand: undefined; + + private _onDidChangeTools = new Emitter(); + readonly onDidChangeTools = this._onDidChangeTools.event; + + private _tools = new Map(); + + constructor( + @IExtensionService private readonly _extensionService: IExtensionService + ) { } + + registerToolData(toolData: IToolData): IDisposable { + if (this._tools.has(toolData.name)) { + throw new Error(`Tool "${toolData.name}" is already registered.`); + } + + this._tools.set(toolData.name, { data: toolData }); + this._onDidChangeTools.fire({ added: toolData }); + + return toDisposable(() => { + this._tools.delete(toolData.name); + this._onDidChangeTools.fire({ removed: toolData.name }); + }); + + } + + registerToolImplementation(name: string, tool: IToolImpl): IDisposable { + const entry = this._tools.get(name); + if (!entry) { + throw new Error(`Tool "${name}" was not contributed.`); + } + + if (entry.impl) { + throw new Error(`Tool "${name}" already has an implementation.`); + } + + entry.impl = tool; + return toDisposable(() => { + entry.impl = undefined; + }); + } + + getTools(): Iterable> { + return Iterable.map(this._tools.values(), i => i.data); + } + + async invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + let tool = this._tools.get(name); + if (!tool) { + throw new Error(`Tool ${name} was not contributed`); + } + + if (!tool.impl) { + await this._extensionService.activateByEvent(`onLanguageModelTool:${name}`); + + // Extension should activate and register the tool implementation + tool = this._tools.get(name); + if (!tool?.impl) { + throw new Error(`Tool ${name} does not have an implementation registered.`); + } + } + + return tool.impl.invoke(parameters, token); + } +} diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 1276da1bd42..b19a50e5e44 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -13,7 +13,6 @@ import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProgress } from 'vs/platform/progress/common/progress'; import { IExtensionService, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -23,14 +22,42 @@ export const enum ChatMessageRole { Assistant, } +export interface IChatMessageTextPart { + type: 'text'; + value: string; +} + +export interface IChatMessageFunctionResultPart { + type: 'function_result'; + name: string; + value: any; + isError?: boolean; +} + +export type IChatMessagePart = IChatMessageTextPart | IChatMessageFunctionResultPart; + export interface IChatMessage { + readonly name?: string | undefined; readonly role: ChatMessageRole; - readonly content: string; + readonly content: IChatMessagePart; +} + +export interface IChatResponseTextPart { + type: 'text'; + value: string; +} + +export interface IChatResponceFunctionUsePart { + type: 'function_use'; + name: string; + parameters: any; } +export type IChatResponsePart = IChatResponseTextPart | IChatResponceFunctionUsePart; + export interface IChatResponseFragment { index: number; - part: string; + part: IChatResponsePart; } export interface ILanguageModelChatMetadata { @@ -51,9 +78,14 @@ export interface ILanguageModelChatMetadata { }; } +export interface ILanguageModelChatResponse { + stream: AsyncIterable; + result: Promise; +} + export interface ILanguageModelChat { metadata: ILanguageModelChatMetadata; - provideChatResponse(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise; + sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, token: CancellationToken): Promise; provideTokenCount(message: string | IChatMessage, token: CancellationToken): Promise; } @@ -91,7 +123,7 @@ export interface ILanguageModelsService { registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable; - makeLanguageModelChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise; + sendChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; computeTokenLength(identifier: string, message: string | IChatMessage, token: CancellationToken): Promise; } @@ -250,12 +282,12 @@ export class LanguageModelsService implements ILanguageModelsService { }); } - makeLanguageModelChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise { + async sendChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise { const provider = this._providers.get(identifier); if (!provider) { throw new Error(`Chat response provider with identifier ${identifier} is not registered.`); } - return provider.provideChatResponse(messages, from, options, progress, token); + return provider.sendChatRequest(messages, from, options, token); } computeTokenLength(identifier: string, message: string | IChatMessage, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts new file mode 100644 index 00000000000..5cc34e72faf --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { DisposableMap } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; + +interface IRawToolContribution { + name: string; + displayName?: string; + description: string; + parametersSchema?: IJSONSchema; +} + +const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'languageModelTools', + activationEventsGenerator: (contributions: IRawToolContribution[], result) => { + for (const contrib of contributions) { + result.push(`onLanguageModelTool:${contrib.name}`); + } + }, + jsonSchema: { + description: localize('vscode.extension.contributes.tools', 'Contributes a tool that can be invoked by a language model.'), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name', 'description'], + properties: { + name: { + description: localize('toolname', "A name for this tool which must be unique across all tools."), + type: 'string' + }, + description: { + description: localize('toolDescription', "A description of this tool that may be passed to a language model."), + type: 'string' + }, + displayName: { + description: localize('toolDisplayName', "A human-readable name for this tool that may be used to describe it in the UI."), + type: 'string' + }, + parametersSchema: { + description: localize('parametersSchema', "A JSON schema for the parameters this tool accepts."), + type: 'object', + $ref: 'http://json-schema.org/draft-07/schema#' + } + } + } + } +}); + +function toToolKey(extensionIdentifier: ExtensionIdentifier, toolName: string) { + return `${extensionIdentifier.value}/${toolName}`; +} + +export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.toolsExtensionPointHandler'; + + private _registrationDisposables = new DisposableMap(); + + constructor( + @ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService, + @ILogService logService: ILogService, + ) { + languageModelToolsExtensionPoint.setHandler((extensions, delta) => { + for (const extension of delta.added) { + for (const tool of extension.value) { + if (!tool.name || !tool.description) { + logService.warn(`Invalid tool contribution from ${extension.description.identifier.value}: ${JSON.stringify(tool)}`); + continue; + } + + const disposable = languageModelToolsService.registerToolData(tool); + this._registrationDisposables.set(toToolKey(extension.description.identifier, tool.name), disposable); + } + } + + for (const extension of delta.removed) { + for (const tool of extension.value) { + this._registrationDisposables.deleteAndDispose(toToolKey(extension.description.identifier, tool.name)); + } + } + }); + } +} diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap index 0d3458d76b7..9bfd3b945e8 100644 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap @@ -1 +1 @@ -
1<canvas>2<details>3</details></canvas>4
\ No newline at end of file +

1<canvas>2</canvas>

<details>3</details>4

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap index 3bb96899c11..c0b5a277aac 100644 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap @@ -1 +1 @@ -
1<details id="id1" style="display: none">2<details id="my id 2">3</details></details>4
\ No newline at end of file +

1

<details id="id1" style="display: none">2<details id="my id 2">3</details></details>4

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap index 34a719b0613..1241ef62b5f 100644 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap @@ -1 +1 @@ -
<img src="http://disallowed.com/image.jpg">
\ No newline at end of file +

<img src="http://disallowed.com/image.jpg">

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap index 023b2e6a846..5b482726d3a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap @@ -1 +1 @@ -
<area>

<input type="text" value="test">
\ No newline at end of file +

<area>



<input type="text" value="test">

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap new file mode 100644 index 00000000000..89991e7676e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap @@ -0,0 +1 @@ +

hello

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap new file mode 100644 index 00000000000..d704b7b322d --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap @@ -0,0 +1,4 @@ +
    +
  1. hello test text
  2. +
+
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts index e9975f25283..657ed1c961c 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts @@ -27,6 +27,18 @@ suite('ChatMarkdownRenderer', () => { await assertSnapshot(result.element.textContent); }); + test('supportHtml with one-line markdown', async () => { + const md = new MarkdownString('**hello**'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + + const md2 = new MarkdownString('1. [_hello_](https://example.com) test **text**'); + md2.supportHtml = true; + const result2 = store.add(testRenderer.render(md2)); + await assertSnapshot(result2.element.outerHTML); + }); + test('invalid HTML', async () => { const md = new MarkdownString('12
3
4'); md.supportHtml = true; diff --git a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts index dcbe7a1782a..2da25bc9817 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts index 21bd0b3930f..f14ffdaa651 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts @@ -8,7 +8,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ChatAgentService, IChatAgentData, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import * as assert from 'assert'; +import assert from 'assert'; const testAgentId = 'testAgent'; const testAgentData: IChatAgentData = { diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index b9f38a04549..e7fb3834f67 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 1e9ad196f8b..cf1f2a174d5 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { assertSnapshot } from 'vs/base/test/common/snapshot'; diff --git a/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts b/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts index 314b4589e31..9330dc5be75 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; @@ -13,7 +13,7 @@ suite('ChatWordCounter', () => { function doTest(str: string, nWords: number, resultStr: string) { const result = getNWords(str, nWords); assert.strictEqual(result.value, resultStr); - assert.strictEqual(result.actualWordCount, nWords); + assert.strictEqual(result.returnedWordCount, nWords); } test('getNWords, matching actualWordCount', () => { diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index 9bf3d63938f..ebdde0c0d22 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -3,12 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; +import { AsyncIterableSource, DeferredPromise, timeout } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; -import { languageModelExtensionPoint, LanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; +import { ChatMessageRole, IChatResponseFragment, languageModelExtensionPoint, LanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -51,7 +53,7 @@ suite('LanguageModels', function () { maxInputTokens: 100, maxOutputTokens: 100, }, - provideChatResponse: async () => { + sendChatRequest: async () => { throw new Error(); }, provideTokenCount: async () => { @@ -70,7 +72,7 @@ suite('LanguageModels', function () { maxInputTokens: 100, maxOutputTokens: 100, }, - provideChatResponse: async () => { + sendChatRequest: async () => { throw new Error(); }, provideTokenCount: async () => { @@ -103,5 +105,56 @@ suite('LanguageModels', function () { assert.deepStrictEqual(result2.length, 0); }); + test('sendChatRequest returns a response-stream', async function () { + store.add(languageModels.registerLanguageModelChat('actual', { + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Pretty Name', + vendor: 'test-vendor', + family: 'actual-family', + version: 'actual-version', + id: 'actual-lm', + maxInputTokens: 100, + maxOutputTokens: 100, + }, + sendChatRequest: async (messages, _from, _options, token) => { + // const message = messages.at(-1); + + const defer = new DeferredPromise(); + const stream = new AsyncIterableSource(); + + (async () => { + while (!token.isCancellationRequested) { + stream.emitOne({ index: 0, part: { type: 'text', value: Date.now().toString() } }); + await timeout(10); + } + defer.complete(undefined); + })(); + + return { + stream: stream.asyncIterable, + result: defer.p + }; + }, + provideTokenCount: async () => { + throw new Error(); + } + })); + + const models = await languageModels.selectLanguageModels({ identifier: 'actual-lm' }); + assert.ok(models.length === 1); + + const first = models[0]; + + const cts = new CancellationTokenSource(); + + const request = await languageModels.sendChatRequest(first, nullExtensionDescription.identifier, [{ role: ChatMessageRole.User, content: { type: 'text', value: 'hello' } }], {}, cts.token); + + assert.ok(request); + + cts.dispose(true); + + await request.result; + }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index 2ff31b0173c..2ad2f91c6b1 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -70,6 +70,7 @@ suite('VoiceChat', () => { getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); } registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise): IDisposable { throw new Error('Method not implemented.'); } getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } + agentHasDupeName(id: string): boolean { throw new Error('Method not implemented.'); } } class TestSpeechService implements ISpeechService { diff --git a/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts b/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts index 249fd8e457c..85d4a9cdb2f 100644 --- a/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts +++ b/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { parseNextChatResponseChunk } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; diff --git a/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts b/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts index 38fd5502ae2..17e6217a6a9 100644 --- a/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts +++ b/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts @@ -8,6 +8,7 @@ import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { Disposable } from 'vs/base/common/lifecycle'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { codeActionCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/common/types'; import * as nls from 'vs/nls'; @@ -34,15 +35,11 @@ const createCodeActionsAutoSave = (description: string): IJSONSchema => { }; }; -const codeActionsOnSaveDefaultProperties = Object.freeze({ - 'source.fixAll': createCodeActionsAutoSave(nls.localize('codeActionsOnSave.fixAll', "Controls whether auto fix action should be run on file save.")), -}); const codeActionsOnSaveSchema: IConfigurationPropertySchema = { oneOf: [ { type: 'object', - properties: codeActionsOnSaveDefaultProperties, additionalProperties: { type: 'string' }, @@ -72,15 +69,24 @@ export const editorConfiguration = Object.freeze({ export class CodeActionsContribution extends Disposable implements IWorkbenchContribution { private _contributedCodeActions: CodeActionsExtensionPoint[] = []; + private settings: Set = new Set(); private readonly _onDidChangeContributions = this._register(new Emitter()); constructor( codeActionsExtensionPoint: IExtensionPoint, @IKeybindingService keybindingService: IKeybindingService, + @ILanguageFeaturesService private readonly languageFeatures: ILanguageFeaturesService ) { super(); + // TODO: @justschen caching of code actions based on extensions loaded: https://github.com/microsoft/vscode/issues/216019 + + languageFeatures.codeActionProvider.onDidChange(() => { + this.updateSettingsFromCodeActionProviders(); + this.updateConfigurationSchemaFromContribs(); + }, 2000); + codeActionsExtensionPoint.setHandler(extensionPoints => { this._contributedCodeActions = extensionPoints.flatMap(x => x.value).filter(x => Array.isArray(x.actions)); this.updateConfigurationSchema(this._contributedCodeActions); @@ -93,9 +99,23 @@ export class CodeActionsContribution extends Disposable implements IWorkbenchCon }); } + private updateSettingsFromCodeActionProviders(): void { + const providers = this.languageFeatures.codeActionProvider.allNoModel(); + providers.forEach(provider => { + if (provider.providedCodeActionKinds) { + provider.providedCodeActionKinds.forEach(kind => { + if (!this.settings.has(kind) && CodeActionKind.Source.contains(new HierarchicalKind(kind))) { + this.settings.add(kind); + } + }); + } + }); + } + private updateConfigurationSchema(codeActionContributions: readonly CodeActionsExtensionPoint[]) { - const newProperties: IJSONSchemaMap = { ...codeActionsOnSaveDefaultProperties }; + const newProperties: IJSONSchemaMap = {}; for (const [sourceAction, props] of this.getSourceActions(codeActionContributions)) { + this.settings.add(sourceAction); newProperties[sourceAction] = createCodeActionsAutoSave(nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", props.title)); } codeActionsOnSaveSchema.properties = newProperties; @@ -103,16 +123,24 @@ export class CodeActionsContribution extends Disposable implements IWorkbenchCon .notifyConfigurationSchemaUpdated(editorConfiguration); } + private updateConfigurationSchemaFromContribs() { + const properties: IJSONSchemaMap = { ...codeActionsOnSaveSchema.properties }; + for (const codeActionKind of this.settings) { + if (!properties[codeActionKind]) { + properties[codeActionKind] = createCodeActionsAutoSave(nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", codeActionKind)); + } + } + codeActionsOnSaveSchema.properties = properties; + Registry.as(Extensions.Configuration) + .notifyConfigurationSchemaUpdated(editorConfiguration); + } + private getSourceActions(contributions: readonly CodeActionsExtensionPoint[]) { - const defaultKinds = Object.keys(codeActionsOnSaveDefaultProperties).map(value => new HierarchicalKind(value)); const sourceActions = new Map(); for (const contribution of contributions) { for (const action of contribution.actions) { const kind = new HierarchicalKind(action.kind); - if (CodeActionKind.Source.contains(kind) - // Exclude any we already included by default - && !defaultKinds.some(defaultKind => defaultKind.contains(kind)) - ) { + if (CodeActionKind.Source.contains(kind)) { sourceActions.set(kind.value, action); } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index 052608edd82..15fba768221 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -33,7 +33,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont const isEmbeddedDiffEditor = this._diffEditor instanceof EmbeddedDiffEditorWidget; if (!isEmbeddedDiffEditor) { - const computationResult = observableFromEvent(e => this._diffEditor.onDidUpdateDiff(e), () => /** @description diffEditor.diffComputationResult */ this._diffEditor.getDiffComputationResult()); + const computationResult = observableFromEvent(this, e => this._diffEditor.onDidUpdateDiff(e), () => /** @description diffEditor.diffComputationResult */ this._diffEditor.getDiffComputationResult()); const onlyWhiteSpaceChange = computationResult.map(r => r && !r.identical && r.changes2.length === 0); this._register(autorunWithStore((reader, store) => { diff --git a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index b047830a881..327554fd781 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -29,33 +29,16 @@ import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLa import { OS } from 'vs/base/common/platform'; import { status } from 'vs/base/browser/ui/aria/aria'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; import { LOG_MODE_ID, OUTPUT_MODE_ID } from 'vs/workbench/services/output/common/output'; import { SEARCH_RESULT_LANGUAGE_ID } from 'vs/workbench/services/search/common/search'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { ChatAgentLocation, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; const $ = dom.$; -// TODO@joyceerhl remove this after a few iterations -Registry.as(Extensions.ConfigurationMigration) - .registerConfigurationMigrations([{ - key: 'workbench.editor.untitled.hint', - migrateFn: (value, _accessor) => ([ - [emptyTextEditorHintSetting, { value }], - ['workbench.editor.untitled.hint', { value: undefined }] - ]) - }, - { - key: 'accessibility.verbosity.untitledHint', - migrateFn: (value, _accessor) => ([ - [AccessibilityVerbositySettingId.EmptyEditorHint, { value }], - ['accessibility.verbosity.untitledHint', { value: undefined }] - ]) - }]); - export interface IEmptyTextEditorHintOptions { readonly clickable?: boolean; } @@ -79,6 +62,7 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { @IChatAgentService private readonly chatAgentService: IChatAgentService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IProductService protected readonly productService: IProductService, + @IContextMenuService private readonly contextMenuService: IContextMenuService ) { this.toDispose = []; this.toDispose.push(this.editor.onDidChangeModel(() => this.update())); @@ -147,7 +131,7 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { } const hasEditorAgents = Boolean(this.chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)); - const shouldRenderDefaultHint = model?.uri.scheme === Schemas.untitled && languageId === PLAINTEXT_LANGUAGE_ID && hasEditorAgents; + const shouldRenderDefaultHint = model?.uri.scheme === Schemas.untitled && languageId === PLAINTEXT_LANGUAGE_ID; return hasEditorAgents || shouldRenderDefaultHint; } @@ -164,7 +148,8 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { this.keybindingService, this.chatAgentService, this.telemetryService, - this.productService + this.productService, + this.contextMenuService ); } else if (!shouldRenderHint && this.textHintContentWidget) { this.textHintContentWidget.dispose(); @@ -197,7 +182,8 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { private readonly keybindingService: IKeybindingService, private readonly chatAgentService: IChatAgentService, private readonly telemetryService: ITelemetryService, - private readonly productService: IProductService + private readonly productService: IProductService, + private readonly contextMenuService: IContextMenuService, ) { this.toDispose = new DisposableStore(); this.toDispose.add(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { @@ -218,6 +204,36 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { return EmptyTextEditorHintContentWidget.ID; } + private _disableHint(e?: MouseEvent) { + const disableHint = () => { + this.configurationService.updateValue(emptyTextEditorHintSetting, 'hidden'); + this.dispose(); + this.editor.focus(); + }; + + if (!e) { + disableHint(); + return; + } + + this.contextMenuService.showContextMenu({ + getAnchor: () => { return new StandardMouseEvent(dom.getActiveWindow(), e); }, + getActions: () => { + return [{ + id: 'workench.action.disableEmptyEditorHint', + label: localize('disableEditorEmptyHint', "Disable Empty Editor Hint"), + tooltip: localize('disableEditorEmptyHint', "Disable Empty Editor Hint"), + enabled: true, + class: undefined, + run: () => { + disableHint(); + } + } + ]; + } + }); + } + private _getHintInlineChat(providers: IChatAgent[]) { const providerName = (providers.length === 1 ? providers[0].fullName : undefined) ?? this.productService.nameShort; @@ -257,6 +273,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { const hintPart = $('a', undefined, fragment); hintPart.style.fontStyle = 'italic'; hintPart.style.cursor = 'pointer'; + this.toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CONTEXT_MENU, (e) => this._disableHint(e))); this.toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CLICK, handleClick)); return hintPart; } else { @@ -275,6 +292,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { if (this.options.clickable) { label.element.style.cursor = 'pointer'; + this.toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CONTEXT_MENU, (e) => this._disableHint(e))); this.toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CLICK, handleClick)); } @@ -297,7 +315,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { hintElement.appendChild(rendered); } - return { ariaLabel, hintHandler, hintElement }; + return { ariaLabel, hintElement }; } private _getHintDefault() { @@ -315,7 +333,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { chooseEditorOnClickOrTap(event.browserEvent); break; case '3': - dontShowOnClickOrTap(); + this._disableHint(); break; } } @@ -360,12 +378,6 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { } }; - const dontShowOnClickOrTap = () => { - this.configurationService.updateValue(emptyTextEditorHintSetting, 'hidden'); - this.dispose(); - this.editor.focus(); - }; - const hintMsg = localize({ key: 'message', comment: [ @@ -387,7 +399,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { anchor.style.cursor = 'pointer'; const id = keybindingsLookup.shift(); const title = id && this.keybindingService.lookupKeybinding(id)?.getLabel(); - hintHandler.disposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), anchor, title ?? '')); + hintHandler.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), anchor, title ?? '')); } return { hintElement, ariaLabel }; diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index bfbdc1d3f50..c7390207969 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -278,9 +278,7 @@ export abstract class SimpleFindWidget extends Widget implements IVerticalSashLa override dispose() { super.dispose(); - if (this._domNode && this._domNode.parentElement) { - this._domNode.parentElement.removeChild(this._domNode); - } + this._domNode?.remove(); } public isVisible(): boolean { diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 6fbc04ff214..e06219499e2 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -162,7 +162,7 @@ export class SuggestEnabledInput extends Widget { const scopedContextKeyService = this.getScopedContextKeyService(contextKeyService); const instantiationService = scopedContextKeyService - ? defaultInstantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])) + ? this._register(defaultInstantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService]))) : defaultInstantiationService; this.inputWidget = this._register(instantiationService.createInstance(CodeEditorWidget, this.stylingContainer, diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts index f5b2c84ebcb..5b5081623cf 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize, localize2 } from 'vs/nls'; +import { Disposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; +import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; export class ToggleMultiCursorModifierAction extends Action2 { @@ -39,7 +40,7 @@ export class ToggleMultiCursorModifierAction extends Action2 { const multiCursorModifier = new RawContextKey('multiCursorModifier', 'altKey'); -class MultiCursorModifierContextKeyController implements IWorkbenchContribution { +class MultiCursorModifierContextKeyController extends Disposable implements IWorkbenchContribution { private readonly _multiCursorModifier: IContextKey; @@ -47,14 +48,15 @@ class MultiCursorModifierContextKeyController implements IWorkbenchContribution @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService ) { + super(); this._multiCursorModifier = multiCursorModifier.bindTo(contextKeyService); this._update(); - configurationService.onDidChangeConfiguration((e) => { + this._register(configurationService.onDidChangeConfiguration((e) => { if (e.affectsConfiguration('editor.multiCursorModifier')) { this._update(); } - }); + })); } private _update(): void { diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts index 7cdd7d910ad..043e3f5c9e7 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts @@ -3,25 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { addDisposableListener, onDidRegisterWindow } from 'vs/base/browser/dom'; +import { mainWindow } from 'vs/base/browser/window'; +import { Codicon } from 'vs/base/common/codicons'; +import { Event } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IActiveCodeEditor, ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution, registerDiffEditorContribution, EditorContributionInstantiation } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, EditorContributionInstantiation, ServicesAccessor, registerDiffEditorContribution, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IDiffEditorContribution, IEditorContribution } from 'vs/editor/common/editorCommon'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; +import * as nls from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { Codicon } from 'vs/base/common/codicons'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { Event } from 'vs/base/common/event'; -import { addDisposableListener, onDidRegisterWindow } from 'vs/base/browser/dom'; -import { mainWindow } from 'vs/base/browser/window'; const transientWordWrapState = 'transientWordWrapState'; const isWordWrapMinifiedKey = 'isWordWrapMinified'; @@ -271,7 +271,7 @@ class EditorWordWrapContextKeyTracker extends Disposable implements IWorkbenchCo disposables.add(addDisposableListener(window, 'focus', () => this._update(), true)); disposables.add(addDisposableListener(window, 'blur', () => this._update(), true)); }, { window: mainWindow, disposables: this._store })); - this._editorService.onDidActiveEditorChange(() => this._update()); + this._register(this._editorService.onDidActiveEditorChange(() => this._update())); this._canToggleWordWrap = CAN_TOGGLE_WORD_WRAP.bindTo(this._contextService); this._editorWordWrap = EDITOR_WORD_WRAP.bindTo(this._contextService); this._activeEditor = null; diff --git a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts index f8960304793..42cc4253ad2 100644 --- a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FinalNewLineParticipant, TrimFinalNewLinesParticipant, TrimWhitespaceParticipant } from 'vs/workbench/contrib/codeEditor/browser/saveParticipants'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index 10b9058bfb9..fe3632fb8d0 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; diff --git a/src/vs/workbench/contrib/comments/browser/commentColors.ts b/src/vs/workbench/contrib/comments/browser/commentColors.ts index 08d44a3224b..b66b9590f76 100644 --- a/src/vs/workbench/contrib/comments/browser/commentColors.ts +++ b/src/vs/workbench/contrib/comments/browser/commentColors.ts @@ -13,11 +13,11 @@ import { IColorTheme } from 'vs/platform/theme/common/themeService'; const resolvedCommentViewIcon = registerColor('commentsView.resolvedIcon', { dark: disabledForeground, light: disabledForeground, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('resolvedCommentIcon', 'Icon color for resolved comments.')); const unresolvedCommentViewIcon = registerColor('commentsView.unresolvedIcon', { dark: listFocusOutline, light: listFocusOutline, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('unresolvedCommentIcon', 'Icon color for unresolved comments.')); -registerColor('editorCommentsWidget.replyInputBackground', { dark: peekViewTitleBackground, light: peekViewTitleBackground, hcDark: peekViewTitleBackground, hcLight: peekViewTitleBackground }, nls.localize('commentReplyInputBackground', 'Background color for comment reply input box.')); +registerColor('editorCommentsWidget.replyInputBackground', peekViewTitleBackground, nls.localize('commentReplyInputBackground', 'Background color for comment reply input box.')); const resolvedCommentBorder = registerColor('editorCommentsWidget.resolvedBorder', { dark: resolvedCommentViewIcon, light: resolvedCommentViewIcon, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('resolvedCommentBorder', 'Color of borders and arrow for resolved comments.')); const unresolvedCommentBorder = registerColor('editorCommentsWidget.unresolvedBorder', { dark: unresolvedCommentViewIcon, light: unresolvedCommentViewIcon, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('unresolvedCommentBorder', 'Color of borders and arrow for unresolved comments.')); -export const commentThreadRangeBackground = registerColor('editorCommentsWidget.rangeBackground', { dark: transparent(unresolvedCommentBorder, .1), light: transparent(unresolvedCommentBorder, .1), hcDark: transparent(unresolvedCommentBorder, .1), hcLight: transparent(unresolvedCommentBorder, .1) }, nls.localize('commentThreadRangeBackground', 'Color of background for comment ranges.')); -export const commentThreadRangeActiveBackground = registerColor('editorCommentsWidget.rangeActiveBackground', { dark: transparent(unresolvedCommentBorder, .1), light: transparent(unresolvedCommentBorder, .1), hcDark: transparent(unresolvedCommentBorder, .1), hcLight: transparent(unresolvedCommentBorder, .1) }, nls.localize('commentThreadActiveRangeBackground', 'Color of background for currently selected or hovered comment range.')); +export const commentThreadRangeBackground = registerColor('editorCommentsWidget.rangeBackground', transparent(unresolvedCommentBorder, .1), nls.localize('commentThreadRangeBackground', 'Color of background for comment ranges.')); +export const commentThreadRangeActiveBackground = registerColor('editorCommentsWidget.rangeActiveBackground', transparent(unresolvedCommentBorder, .1), nls.localize('commentThreadActiveRangeBackground', 'Color of background for currently selected or hovered comment range.')); const commentThreadStateBorderColors = new Map([ [languages.CommentThreadState.Unresolved, unresolvedCommentBorder], diff --git a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts index 92b52ac5402..725b522f956 100644 --- a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts @@ -14,11 +14,11 @@ import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { CommentThreadState } from 'vs/editor/common/languages'; export const overviewRulerCommentingRangeForeground = registerColor('editorGutter.commentRangeForeground', { dark: opaque(listInactiveSelectionBackground, editorBackground), light: darken(opaque(listInactiveSelectionBackground, editorBackground), .05), hcDark: Color.white, hcLight: Color.black }, nls.localize('editorGutterCommentRangeForeground', 'Editor gutter decoration color for commenting ranges. This color should be opaque.')); -const overviewRulerCommentForeground = registerColor('editorOverviewRuler.commentForeground', { dark: overviewRulerCommentingRangeForeground, light: overviewRulerCommentingRangeForeground, hcDark: overviewRulerCommentingRangeForeground, hcLight: overviewRulerCommentingRangeForeground }, nls.localize('editorOverviewRuler.commentForeground', 'Editor overview ruler decoration color for resolved comments. This color should be opaque.')); -const overviewRulerCommentUnresolvedForeground = registerColor('editorOverviewRuler.commentUnresolvedForeground', { dark: overviewRulerCommentForeground, light: overviewRulerCommentForeground, hcDark: overviewRulerCommentForeground, hcLight: overviewRulerCommentForeground }, nls.localize('editorOverviewRuler.commentUnresolvedForeground', 'Editor overview ruler decoration color for unresolved comments. This color should be opaque.')); +const overviewRulerCommentForeground = registerColor('editorOverviewRuler.commentForeground', overviewRulerCommentingRangeForeground, nls.localize('editorOverviewRuler.commentForeground', 'Editor overview ruler decoration color for resolved comments. This color should be opaque.')); +const overviewRulerCommentUnresolvedForeground = registerColor('editorOverviewRuler.commentUnresolvedForeground', overviewRulerCommentForeground, nls.localize('editorOverviewRuler.commentUnresolvedForeground', 'Editor overview ruler decoration color for unresolved comments. This color should be opaque.')); const editorGutterCommentGlyphForeground = registerColor('editorGutter.commentGlyphForeground', { dark: editorForeground, light: editorForeground, hcDark: Color.black, hcLight: Color.white }, nls.localize('editorGutterCommentGlyphForeground', 'Editor gutter decoration color for commenting glyphs.')); -registerColor('editorGutter.commentUnresolvedGlyphForeground', { dark: editorGutterCommentGlyphForeground, light: editorGutterCommentGlyphForeground, hcDark: editorGutterCommentGlyphForeground, hcLight: editorGutterCommentGlyphForeground }, nls.localize('editorGutterCommentUnresolvedGlyphForeground', 'Editor gutter decoration color for commenting glyphs for unresolved comment threads.')); +registerColor('editorGutter.commentUnresolvedGlyphForeground', editorGutterCommentGlyphForeground, nls.localize('editorGutterCommentUnresolvedGlyphForeground', 'Editor gutter decoration color for commenting glyphs for unresolved comment threads.')); export class CommentGlyphWidget { public static description = 'comment-glyph-widget'; diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index 9c9c8e24e50..3868281f8e5 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -121,7 +121,7 @@ export class CommentReply extends Disposable { this.expandReplyArea(); } else if (hasExistingComments) { this.createReplyButton(this.commentEditor, this.form); - } else if (focus && (this._commentThread.comments && this._commentThread.comments.length === 0)) { + } else if (focus && (!this._commentThread.comments || this._commentThread.comments.length === 0)) { this.expandReplyArea(); } this._error = dom.append(this.form, dom.$('.validation-error.hidden')); @@ -368,7 +368,7 @@ export class CommentReply extends Disposable { private createReplyButton(commentEditor: ICodeEditor, commentForm: HTMLElement) { this._reviewThreadReplyButton = dom.append(commentForm, dom.$(`button.review-thread-reply-button.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this._reviewThreadReplyButton, this._commentOptions?.prompt || nls.localize('reply', "Reply..."))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this._reviewThreadReplyButton, this._commentOptions?.prompt || nls.localize('reply', "Reply..."))); this._reviewThreadReplyButton.textContent = this._commentOptions?.prompt || nls.localize('reply', "Reply..."); // bind click/escape actions for reviewThreadReplyButton and textArea diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts index 14db2828ee2..3728fb02731 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts @@ -181,7 +181,7 @@ export class CommentThreadBody extends D this._commentDisposable.delete(commentToDelete); this._commentElements.splice(commentElementsToDelIndex[i], 1); - this._commentsElement.removeChild(commentToDelete.domNode); + commentToDelete.domNode.remove(); } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts index 2849c9afd76..8333654958e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts @@ -7,7 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, ActionRunner } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import * as languages from 'vs/editor/common/languages'; import { IRange } from 'vs/editor/common/core/range'; @@ -46,6 +46,7 @@ export class CommentThreadHeader extends Disposable { super(); this._headElement = dom.$('.head'); container.appendChild(this._headElement); + this._register(toDisposable(() => this._headElement.remove())); this._fillHead(); } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index a919fe262a9..521455bb77a 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/review'; import * as dom from 'vs/base/browser/dom'; import { Emitter } from 'vs/base/common/event'; -import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import * as languages from 'vs/editor/common/languages'; import { IMarkdownRendererOptions } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; @@ -104,6 +104,7 @@ export class CommentThreadWidget extends const bodyElement = dom.$('.body'); container.appendChild(bodyElement); + this._register(toDisposable(() => bodyElement.remove())); const tracker = this._register(dom.trackFocus(bodyElement)); this._register(registerNavigableContainer({ diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index a79ec4c2b3c..2c909a2ed5e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -139,9 +139,9 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget super(editor, { keepEditorSelection: true, isAccessible: true }); this._contextKeyService = contextKeyService.createScoped(this.domNode); - this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection( + this._scopedInstantiationService = this._globalToDispose.add(instantiationService.createChild(new ServiceCollection( [IContextKeyService, this._contextKeyService] - )); + ))); const controller = this.commentService.getCommentController(this._uniqueOwner); if (controller) { diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index d679c223423..09aeddd25e6 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -430,6 +430,7 @@ export class CommentController implements IEditorContribution { private _commentingRangeSpaceReserved = false; private _commentingRangeAmountReserved = 0; private _computePromise: CancelablePromise> | null; + private _computeAndSetPromise: Promise | undefined; private _addInProgress!: boolean; private _emptyThreadsToAddQueue: [Range | undefined, IEditorMouseEvent | undefined][] = []; private _computeCommentingRangePromise!: CancelablePromise | null; @@ -645,10 +646,12 @@ export class CommentController implements IEditorContribution { return Promise.resolve([]); }); - return this._computePromise.then(async commentInfos => { + this._computeAndSetPromise = this._computePromise.then(async commentInfos => { await this.setComments(coalesce(commentInfos)); this._computePromise = null; }, error => console.log(error)); + this._computePromise.then(() => this._computeAndSetPromise = undefined); + return this._computeAndSetPromise; } private beginComputeCommentingRanges() { @@ -687,8 +690,8 @@ export class CommentController implements IEditorContribution { if (commentThreadWidget.length === 1) { commentThreadWidget[0].reveal(commentUniqueId, focus); } else if (fetchOnceIfNotExist) { - if (this._computePromise) { - this._computePromise.then(_ => { + if (this._computeAndSetPromise) { + this._computeAndSetPromise.then(_ => { this.revealCommentThread(threadId, commentUniqueId, false, focus); }); } else { @@ -728,7 +731,7 @@ export class CommentController implements IEditorContribution { return; } - const after = this.editor.getSelection().getEndPosition(); + const after = reverse ? this.editor.getSelection().getStartPosition() : this.editor.getSelection().getEndPosition(); const sortedWidgets = this._commentWidgets.sort((a, b) => { if (reverse) { const temp = a; @@ -866,7 +869,8 @@ export class CommentController implements IEditorContribution { const pendingCommentText = (this._pendingNewCommentCache[uniqueOwner] && this._pendingNewCommentCache[uniqueOwner][thread.threadId]) ?? continueOnCommentText; const pendingEdits = this._pendingEditsCache[uniqueOwner] && this._pendingEditsCache[uniqueOwner][thread.threadId]; - const shouldReveal = thread.canReply && thread.isTemplate && (!thread.comments || (thread.comments.length === 0)) && (!thread.editorId || (thread.editorId === editorId)); + const isThreadTemplateOrEmpty = (thread.isTemplate || (!thread.comments || (thread.comments.length === 0))); + const shouldReveal = thread.canReply && isThreadTemplateOrEmpty && (!thread.editorId || (thread.editorId === editorId)); await this.displayCommentThread(uniqueOwner, thread, shouldReveal, pendingCommentText, pendingEdits); this._commentInfos.filter(info => info.uniqueOwner === uniqueOwner)[0].threads.push(thread); this.tryUpdateReservedSpace(); diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 6caf357feb6..b527edf7bde 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -303,7 +303,7 @@ export class CommentNodeRenderer implements IListRenderer const renderedComment = this.getRenderedComment(originalComment.comment.body, disposables); templateData.disposables.push(renderedComment); templateData.threadMetadata.commentPreview.appendChild(renderedComment.element.firstElementChild ?? renderedComment.element); - templateData.disposables.push(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), templateData.threadMetadata.commentPreview, renderedComment.element.textContent ?? '')); + templateData.disposables.push(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), templateData.threadMetadata.commentPreview, renderedComment.element.textContent ?? '')); } if (node.element.range) { diff --git a/src/vs/workbench/contrib/comments/browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css index 938c658fd2d..8527341bdae 100644 --- a/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/src/vs/workbench/contrib/comments/browser/media/panel.css @@ -90,11 +90,14 @@ .comments-panel .comments-panel-container .tree-container .comment-thread-container .text * { margin: 0; text-overflow: ellipsis; - max-width: 500px; overflow: hidden; padding-right: 5px; } +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata .text * { + max-width: 700px; +} + .comments-panel .comments-panel-container .tree-container .comment-thread-container .range { opacity: 0.8; } diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 0ee64b833ea..f643c68547f 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -244,11 +244,6 @@ margin-top: 0; } -.review-widget .body .comment-body code { - border-radius: 3px; - padding: 0 0.4em; -} - .review-widget .body .comment-body span { white-space: pre; } diff --git a/src/vs/workbench/contrib/comments/browser/timestamp.ts b/src/vs/workbench/contrib/comments/browser/timestamp.ts index 47faa02f1c9..583ccaa7963 100644 --- a/src/vs/workbench/contrib/comments/browser/timestamp.ts +++ b/src/vs/workbench/contrib/comments/browser/timestamp.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { fromNow } from 'vs/base/common/date'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -18,7 +18,7 @@ export class TimestampWidget extends Disposable { private _timestamp: Date | undefined; private _useRelativeTime: boolean; - private hover: IUpdatableHover; + private hover: IManagedHover; constructor( private configurationService: IConfigurationService, @@ -30,7 +30,7 @@ export class TimestampWidget extends Disposable { this._date = dom.append(container, dom.$('span.timestamp')); this._date.style.display = 'none'; this._useRelativeTime = this.useRelativeTimeSetting; - this.hover = this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this._date, '')); + this.hover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this._date, '')); this.setTimestamp(timeStamp); } diff --git a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts index 84e77afe298..9c7d9e80c16 100644 --- a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts +++ b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IRange, Range } from 'vs/editor/common/core/range'; import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; diff --git a/src/vs/workbench/contrib/contextmenu/browser/contextmenu.contribution.ts b/src/vs/workbench/contrib/contextmenu/browser/contextmenu.contribution.ts index e903b24dc39..3f607d926f3 100644 --- a/src/vs/workbench/contrib/contextmenu/browser/contextmenu.contribution.ts +++ b/src/vs/workbench/contrib/contextmenu/browser/contextmenu.contribution.ts @@ -3,24 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -class ContextMenuContribution implements IWorkbenchContribution { - - private readonly disposables = new DisposableStore(); +class ContextMenuContribution extends Disposable implements IWorkbenchContribution { constructor( @ILayoutService layoutService: ILayoutService, @IContextMenuService contextMenuService: IContextMenuService ) { + super(); + const update = (visible: boolean) => layoutService.activeContainer.classList.toggle('context-menu-visible', visible); - contextMenuService.onDidShowContextMenu(() => update(true), null, this.disposables); - contextMenuService.onDidHideContextMenu(() => update(false), null, this.disposables); + this._register(contextMenuService.onDidShowContextMenu(() => update(true))); + this._register(contextMenuService.onDidHideContextMenu(() => update(false))); } } diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index 3546424023c..0515cc780f3 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -49,6 +49,7 @@ export interface IRenderValueOptions { export interface IVariableTemplateData { expression: HTMLElement; name: HTMLElement; + type: HTMLElement; value: HTMLElement; label: HighlightedLabel; lazyButton: HTMLElement; @@ -109,7 +110,7 @@ export function renderExpressionValue(expressionOrValue: IExpressionValue | stri if (options.hover) { const { store, commands, commandService } = options.hover instanceof DisposableStore ? { store: options.hover, commands: [], commandService: undefined } : options.hover; - store.add(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), container, () => { + store.add(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, () => { const container = dom.$('div'); const markdownHoverElement = dom.$('div.hover-row'); const hoverContentsElement = dom.append(markdownHoverElement, dom.$('div.hover-contents')); @@ -130,13 +131,20 @@ export function renderExpressionValue(expressionOrValue: IExpressionValue | stri } } -export function renderVariable(store: DisposableStore, commandService: ICommandService, hoverService: IHoverService, variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[], linkDetector?: LinkDetector): void { +export function renderVariable(store: DisposableStore, commandService: ICommandService, hoverService: IHoverService, variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[], linkDetector?: LinkDetector, displayType?: boolean): void { if (variable.available) { + data.type.textContent = ''; let text = variable.name; if (variable.value && typeof variable.name === 'string') { - text += ':'; + if (variable.type && displayType) { + text += ': '; + data.type.textContent = variable.type + ' ='; + } else { + text += ' ='; + } } - data.label.set(text, highlights, variable.type ? variable.type : variable.name); + + data.label.set(text, highlights, variable.type && !displayType ? variable.type : variable.name); data.name.classList.toggle('virtual', variable.presentationHint?.kind === 'virtual'); data.name.classList.toggle('internal', variable.presentationHint?.visibility === 'internal'); } else if (variable.value && typeof variable.name === 'string' && variable.name) { @@ -171,6 +179,7 @@ export interface IInputBoxOptions { export interface IExpressionTemplateData { expression: HTMLElement; name: HTMLSpanElement; + type: HTMLSpanElement; value: HTMLSpanElement; inputBoxContainer: HTMLElement; actionBar?: ActionBar; @@ -228,7 +237,10 @@ export abstract class AbstractExpressionsRenderer implements IT const name = dom.append(expression, $('span.name')); const lazyButton = dom.append(expression, $('span.lazy-button')); lazyButton.classList.add(...ThemeIcon.asClassNameArray(Codicon.eye)); - templateDisposable.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), lazyButton, localize('debug.lazyButton.tooltip', "Click to expand"))); + + templateDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), lazyButton, localize('debug.lazyButton.tooltip', "Click to expand"))); + const type = dom.append(expression, $('span.type')); + const value = dom.append(expression, $('span.value')); const label = templateDisposable.add(new HighlightedLabel(name)); @@ -241,7 +253,7 @@ export abstract class AbstractExpressionsRenderer implements IT actionBar = templateDisposable.add(new ActionBar(expression)); } - const template: IExpressionTemplateData = { expression, name, value, label, inputBoxContainer, actionBar, elementDisposable: new DisposableStore(), templateDisposable, lazyButton, currentElement: undefined }; + const template: IExpressionTemplateData = { expression, name, type, value, label, inputBoxContainer, actionBar, elementDisposable: new DisposableStore(), templateDisposable, lazyButton, currentElement: undefined }; templateDisposable.add(dom.addDisposableListener(lazyButton, dom.EventType.CLICK, () => { if (template.currentElement) { @@ -255,7 +267,6 @@ export abstract class AbstractExpressionsRenderer implements IT public abstract renderElement(node: ITreeNode, index: number, data: IExpressionTemplateData): void; protected renderExpressionElement(element: IExpression, node: ITreeNode, data: IExpressionTemplateData): void { - data.elementDisposable.clear(); data.currentElement = element; this.renderExpression(node.element, data, createMatches(node.filterData)); if (data.actionBar) { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 1b53d40047f..d29256d9072 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -869,8 +869,8 @@ registerThemingParticipant((theme, collector) => { } }); -export const debugIconBreakpointForeground = registerColor('debugIcon.breakpointForeground', { dark: '#E51400', light: '#E51400', hcDark: '#E51400', hcLight: '#E51400' }, nls.localize('debugIcon.breakpointForeground', 'Icon color for breakpoints.')); -const debugIconBreakpointDisabledForeground = registerColor('debugIcon.breakpointDisabledForeground', { dark: '#848484', light: '#848484', hcDark: '#848484', hcLight: '#848484' }, nls.localize('debugIcon.breakpointDisabledForeground', 'Icon color for disabled breakpoints.')); -const debugIconBreakpointUnverifiedForeground = registerColor('debugIcon.breakpointUnverifiedForeground', { dark: '#848484', light: '#848484', hcDark: '#848484', hcLight: '#848484' }, nls.localize('debugIcon.breakpointUnverifiedForeground', 'Icon color for unverified breakpoints.')); +export const debugIconBreakpointForeground = registerColor('debugIcon.breakpointForeground', '#E51400', nls.localize('debugIcon.breakpointForeground', 'Icon color for breakpoints.')); +const debugIconBreakpointDisabledForeground = registerColor('debugIcon.breakpointDisabledForeground', '#848484', nls.localize('debugIcon.breakpointDisabledForeground', 'Icon color for disabled breakpoints.')); +const debugIconBreakpointUnverifiedForeground = registerColor('debugIcon.breakpointUnverifiedForeground', '#848484', nls.localize('debugIcon.breakpointUnverifiedForeground', 'Icon color for unverified breakpoints.')); const debugIconBreakpointCurrentStackframeForeground = registerColor('debugIcon.breakpointCurrentStackframeForeground', { dark: '#FFCC00', light: '#BE8700', hcDark: '#FFCC00', hcLight: '#BE8700' }, nls.localize('debugIcon.breakpointCurrentStackframeForeground', 'Icon color for the current breakpoint stack frame.')); -const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', { dark: '#89D185', light: '#89D185', hcDark: '#89D185', hcLight: '#89D185' }, nls.localize('debugIcon.breakpointStackframeForeground', 'Icon color for all breakpoint stack frames.')); +const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', '#89D185', nls.localize('debugIcon.breakpointStackframeForeground', 'Icon color for all breakpoint stack frames.')); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 59a3a4dd4bf..11a82d72c3a 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -264,7 +264,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi } private createTriggerBreakpointInput(container: HTMLElement) { - const breakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp !== this.breakpoint); + const breakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp !== this.breakpoint && !bp.logMessage); const breakpointOptions: ISelectOptionItem[] = [ { text: nls.localize('noTriggerByBreakpoint', 'None'), isDisabled: true }, ...breakpoints.map(bp => ({ @@ -346,7 +346,10 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.toDispose.push(scopedContextKeyService); const scopedInstatiationService = this.instantiationService.createChild(new ServiceCollection( - [IContextKeyService, scopedContextKeyService], [IPrivateBreakpointWidgetService, this])); + [IContextKeyService, scopedContextKeyService], + [IPrivateBreakpointWidgetService, this] + )); + this.toDispose.push(scopedInstatiationService); const options = this.createEditorOptions(); const codeEditorWidgetOptions = getSimpleCodeEditorWidgetOptions(); @@ -433,12 +436,12 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi if (success) { // if there is already a breakpoint on this location - remove it. - let condition = this.breakpoint?.condition; - let hitCondition = this.breakpoint?.hitCondition; - let logMessage = this.breakpoint?.logMessage; - let triggeredBy = this.breakpoint?.triggeredBy; - let mode = this.breakpoint?.mode; - let modeLabel = this.breakpoint?.modeLabel; + let condition: string | undefined = undefined; + let hitCondition: string | undefined = undefined; + let logMessage: string | undefined = undefined; + let triggeredBy: string | undefined = undefined; + let mode: string | undefined = undefined; + let modeLabel: string | undefined = undefined; this.rememberInput(); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 8b989fb46f4..ac8dc3ed3d0 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -55,6 +55,7 @@ import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, In import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; const $ = dom.$; @@ -553,7 +554,7 @@ class BreakpointsRenderer implements IListRenderer { const debugService = accessor.get(IDebugService); + const viewService = accessor.get(IViewsService); + await viewService.openView(BREAKPOINTS_VIEW_ID); debugService.addFunctionBreakpoint(); } }); @@ -1643,7 +1646,7 @@ registerAction2(class extends Action2 { } else if (breakpoint instanceof DataBreakpoint) { await debugService.removeDataBreakpoints(breakpoint.getId()); } else if (breakpoint instanceof InstructionBreakpoint) { - await debugService.removeInstructionBreakpoints(breakpoint.instructionReference); + await debugService.removeInstructionBreakpoints(breakpoint.instructionReference, breakpoint.offset); } } }); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 80bdc5ffbd8..24eba761682 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -49,7 +49,7 @@ import { CALLSTACK_VIEW_ID, CONTEXT_CALLSTACK_ITEM_STOPPED, CONTEXT_CALLSTACK_IT import { StackFrame, Thread, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; const $ = dom.$; @@ -136,7 +136,7 @@ async function expandTo(session: IDebugSession, tree: WorkbenchCompressibleAsync export class CallStackView extends ViewPane { private stateMessage!: HTMLSpanElement; private stateMessageLabel!: HTMLSpanElement; - private stateMessageLabelHover!: IUpdatableHover; + private stateMessageLabelHover!: IManagedHover; private onCallStackChangeScheduler: RunOnceScheduler; private needsRefresh = false; private ignoreSelectionChangedEvent = false; @@ -221,7 +221,7 @@ export class CallStackView extends ViewPane { this.stateMessage = dom.append(container, $('span.call-stack-state-message')); this.stateMessage.hidden = true; this.stateMessageLabel = dom.append(this.stateMessage, $('span.label')); - this.stateMessageLabelHover = this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.stateMessage, '')); + this.stateMessageLabelHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.stateMessage, '')); } protected override renderBody(container: HTMLElement): void { @@ -582,7 +582,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer t.stopped); @@ -671,7 +671,7 @@ class ThreadsRenderer implements ICompressibleTreeRenderer, _index: number, data: IThreadTemplateData): void { const thread = element.element; - data.elementDisposable.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), data.thread, thread.name)); + data.elementDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.thread, thread.name)); data.label.set(thread.name, createMatches(element.filterData)); data.stateLabel.textContent = thread.stateLabel; data.stateLabel.classList.toggle('exception', thread.stoppedDetails?.reason === 'exception'); @@ -756,7 +756,7 @@ class StackFramesRenderer implements ICompressibleTreeRenderer, index: number, data: IErrorTemplateData): void { const error = element.element; data.label.textContent = error; - data.templateDisposable.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), data.label, error)); + data.templateDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.label, error)); } renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: IErrorTemplateData, height: number | undefined): void { diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 94947bacc7e..b783a8079ba 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -445,6 +445,11 @@ configurationRegistry.registerConfiguration({ title: nls.localize('debugConfigurationTitle', "Debug"), type: 'object', properties: { + 'debug.showVariableTypes': { + type: 'boolean', + description: nls.localize({ comment: ['This is the description for a setting'], key: 'showVariableTypes' }, "Show variable type in variable pane during debug session"), + default: false + }, 'debug.allowBreakpointsEverywhere': { type: 'boolean', description: nls.localize({ comment: ['This is the description for a setting'], key: 'allowBreakpointsEverywhere' }, "Allow setting breakpoints in any file."), @@ -485,7 +490,7 @@ configurationRegistry.registerConfiguration({ }, 'debug.toolBarLocation': { enum: ['floating', 'docked', 'commandCenter', 'hidden'], - markdownDescription: nls.localize({ comment: ['This is the description for a setting'], key: 'toolBarLocation' }, "Controls the location of the debug toolbar. Either `floating` in all views, `docked` in the debug view, `commandCenter` (requires `{0}`), or `hidden`.", '#window.commandCenter#'), + markdownDescription: nls.localize({ comment: ['This is the description for a setting'], key: 'toolBarLocation' }, "Controls the location of the debug toolbar. Either `floating` in all views, `docked` in the debug view, `commandCenter` (requires {0}), or `hidden`.", '`#window.commandCenter#`'), default: 'floating', markdownEnumDescriptions: [ nls.localize('debugToolBar.floating', "Show debug toolbar in all views."), @@ -632,7 +637,7 @@ configurationRegistry.registerConfiguration({ }, 'debug.hideLauncherWhileDebugging': { type: 'boolean', - markdownDescription: nls.localize({ comment: ['This is the description for a setting'], key: 'debug.hideLauncherWhileDebugging' }, "Hide 'Start Debugging' control in title bar of 'Run and Debug' view while debugging is active. Only relevant when `{0}` is not `docked`.", '#debug.toolBarLocation#'), + markdownDescription: nls.localize({ comment: ['This is the description for a setting'], key: 'debug.hideLauncherWhileDebugging' }, "Hide 'Start Debugging' control in title bar of 'Run and Debug' view while debugging is active. Only relevant when {0} is not `docked`.", '`#debug.toolBarLocation#`'), default: false } } diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index a4d5c4e74e2..50a1b351fc2 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -78,7 +78,7 @@ export class StartDebugActionViewItem extends BaseActionViewItem { const keybinding = this.keybindingService.lookupKeybinding(this.action.id)?.getLabel(); const keybindingLabel = keybinding ? ` (${keybinding})` : ''; const title = this.action.label + keybindingLabel; - this.toDispose.push(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.start, title)); + this.toDispose.push(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.start, title)); this.start.setAttribute('role', 'button'); this.start.ariaLabel = title; diff --git a/src/vs/workbench/contrib/debug/browser/debugColors.ts b/src/vs/workbench/contrib/debug/browser/debugColors.ts index 1af9c0b359b..ae932df5351 100644 --- a/src/vs/workbench/contrib/debug/browser/debugColors.ts +++ b/src/vs/workbench/contrib/debug/browser/debugColors.ts @@ -18,12 +18,7 @@ export const debugToolBarBackground = registerColor('debugToolBar.background', { hcLight: '#FFFFFF' }, localize('debugToolBarBackground', "Debug toolbar background color.")); -export const debugToolBarBorder = registerColor('debugToolBar.border', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('debugToolBarBorder', "Debug toolbar border color.")); +export const debugToolBarBorder = registerColor('debugToolBar.border', null, localize('debugToolBarBorder', "Debug toolbar border color.")); export const debugIconStartForeground = registerColor('debugIcon.startForeground', { dark: '#89D185', @@ -37,6 +32,7 @@ export function registerColors() { // --- Start Positron --- // Renamed `"Variables"` -> `"Debug Variables"` to avoid confusion with session Variables pane const debugTokenExpressionName = registerColor('debugTokenExpression.name', { dark: '#c586c0', light: '#9b46b0', hcDark: foreground, hcLight: foreground }, 'Foreground color for the token names shown in the debug views (ie. the Debug Variables or Watch view).'); + const debugTokenExpressionType = registerColor('debugTokenExpression.type', { dark: '#4A90E2', light: '#4A90E2', hcDark: foreground, hcLight: foreground }, 'Foreground color for the token types shown in the debug views (ie. the Debug Variables or Watch view).'); const debugTokenExpressionValue = registerColor('debugTokenExpression.value', { dark: '#cccccc99', light: '#6c6c6ccc', hcDark: foreground, hcLight: foreground }, 'Foreground color for the token values shown in the debug views (ie. the Debug Variables or Watch view).'); const debugTokenExpressionString = registerColor('debugTokenExpression.string', { dark: '#ce9178', light: '#a31515', hcDark: '#f48771', hcLight: '#a31515' }, 'Foreground color for strings in the debug views (ie. the Debug Variables or Watch view).'); const debugTokenExpressionBoolean = registerColor('debugTokenExpression.boolean', { dark: '#4e94ce', light: '#0000ff', hcDark: '#75bdfe', hcLight: '#0000ff' }, 'Foreground color for booleans in the debug views (ie. the Debug Variables or Watch view).'); @@ -46,18 +42,18 @@ export function registerColors() { const debugViewExceptionLabelForeground = registerColor('debugView.exceptionLabelForeground', { dark: foreground, light: '#FFF', hcDark: foreground, hcLight: foreground }, 'Foreground color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); const debugViewExceptionLabelBackground = registerColor('debugView.exceptionLabelBackground', { dark: '#6C2022', light: '#A31515', hcDark: '#6C2022', hcLight: '#A31515' }, 'Background color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); - const debugViewStateLabelForeground = registerColor('debugView.stateLabelForeground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, 'Foreground color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); - const debugViewStateLabelBackground = registerColor('debugView.stateLabelBackground', { dark: '#88888844', light: '#88888844', hcDark: '#88888844', hcLight: '#88888844' }, 'Background color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); + const debugViewStateLabelForeground = registerColor('debugView.stateLabelForeground', foreground, 'Foreground color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); + const debugViewStateLabelBackground = registerColor('debugView.stateLabelBackground', '#88888844', 'Background color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); // --- Start Positron --- // Renamed `"Variables"` -> `"Debug Variables"` to avoid confusion with session Variables pane - const debugViewValueChangedHighlight = registerColor('debugView.valueChangedHighlight', { dark: '#569CD6', light: '#569CD6', hcDark: '#569CD6', hcLight: '#569CD6' }, 'Color used to highlight value changes in the debug views (ie. in the Debug Variables view).'); + const debugViewValueChangedHighlight = registerColor('debugView.valueChangedHighlight', '#569CD6', 'Color used to highlight value changes in the debug views (ie. in the Debug Variables view).'); // --- End Positron --- const debugConsoleInfoForeground = registerColor('debugConsole.infoForeground', { dark: editorInfoForeground, light: editorInfoForeground, hcDark: foreground, hcLight: foreground }, 'Foreground color for info messages in debug REPL console.'); const debugConsoleWarningForeground = registerColor('debugConsole.warningForeground', { dark: editorWarningForeground, light: editorWarningForeground, hcDark: '#008000', hcLight: editorWarningForeground }, 'Foreground color for warning messages in debug REPL console.'); - const debugConsoleErrorForeground = registerColor('debugConsole.errorForeground', { dark: errorForeground, light: errorForeground, hcDark: errorForeground, hcLight: errorForeground }, 'Foreground color for error messages in debug REPL console.'); - const debugConsoleSourceForeground = registerColor('debugConsole.sourceForeground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, 'Foreground color for source filenames in debug REPL console.'); - const debugConsoleInputIconForeground = registerColor('debugConsoleInputIcon.foreground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, 'Foreground color for debug console input marker icon.'); + const debugConsoleErrorForeground = registerColor('debugConsole.errorForeground', errorForeground, 'Foreground color for error messages in debug REPL console.'); + const debugConsoleSourceForeground = registerColor('debugConsole.sourceForeground', foreground, 'Foreground color for source filenames in debug REPL console.'); + const debugConsoleInputIconForeground = registerColor('debugConsoleInputIcon.foreground', foreground, 'Foreground color for debug console input marker icon.'); const debugIconPauseForeground = registerColor('debugIcon.pauseForeground', { dark: '#75BEFF', @@ -216,6 +212,7 @@ export function registerColors() { } const tokenNameColor = theme.getColor(debugTokenExpressionName)!; + const tokenTypeColor = theme.getColor(debugTokenExpressionType)!; const tokenValueColor = theme.getColor(debugTokenExpressionValue)!; const tokenStringColor = theme.getColor(debugTokenExpressionString)!; const tokenBooleanColor = theme.getColor(debugTokenExpressionBoolean)!; @@ -227,6 +224,10 @@ export function registerColors() { color: ${tokenNameColor}; } + .monaco-workbench .monaco-list-row .expression .type { + color: ${tokenTypeColor}; + } + .monaco-workbench .monaco-list-row .expression .value, .monaco-workbench .debug-hover-widget .value { color: ${tokenValueColor}; diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index 7810995bb38..4f613027aaf 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -14,6 +14,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import * as nls from 'vs/nls'; +import { ILocalizedString } from 'vs/platform/action/common/action'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -22,14 +23,14 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { PanelFocusContext } from 'vs/workbench/common/contextkeys'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { openBreakpointSource } from 'vs/workbench/contrib/debug/browser/breakpointsView'; import { DisassemblyView } from 'vs/workbench/contrib/debug/browser/disassemblyView'; -import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE, CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_EXCEPTION_WIDGET_VISIBLE, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_IN_DEBUG_MODE, CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, IDebugConfiguration, IDebugEditorContribution, IDebugService, REPL_VIEW_ID, WATCH_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { Repl } from 'vs/workbench/contrib/debug/browser/repl'; +import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_EXCEPTION_WIDGET_VISIBLE, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_IN_DEBUG_MODE, CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, IDebugConfiguration, IDebugEditorContribution, IDebugService, REPL_VIEW_ID, WATCH_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { getEvaluatableExpressionAtPosition } from 'vs/workbench/contrib/debug/common/debugUtils'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ILocalizedString } from 'vs/platform/action/common/action'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; class ToggleBreakpointAction extends Action2 { constructor() { @@ -39,6 +40,7 @@ class ToggleBreakpointAction extends Action2 { ...nls.localize2('toggleBreakpointAction', "Debug: Toggle Breakpoint"), mnemonicTitle: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint"), }, + f1: true, precondition: CONTEXT_DEBUGGERS_AVAILABLE, keybinding: { when: ContextKeyExpr.or(EditorContextKeys.editorTextFocus, CONTEXT_DISASSEMBLY_VIEW_FOCUS), @@ -368,8 +370,8 @@ export class SelectionToReplAction extends EditorAction { text = editor.getModel().getValueInRange(selection); } - await session.addReplExpression(viewModel.focusedStackFrame, text); - await viewsService.openView(REPL_VIEW_ID, false); + const replView = await viewsService.openView(REPL_VIEW_ID, false) as Repl | undefined; + replView?.sendReplInput(text); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 4ab2ba9d777..5e3d5e9d583 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -6,7 +6,7 @@ import { addDisposableListener, isKeyboardEvent } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { distinct } from 'vs/base/common/arrays'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { memoize } from 'vs/base/common/decorators'; @@ -66,12 +66,7 @@ export const debugInlineForeground = registerColor('editor.inlineValuesForegroun hcLight: '#00000080' }, nls.localize('editor.inlineValuesForeground', "Color for the debug inline value text.")); -export const debugInlineBackground = registerColor('editor.inlineValuesBackground', { - dark: '#ffc80033', - light: '#ffc80033', - hcDark: '#ffc80033', - hcLight: '#ffc80033' -}, nls.localize('editor.inlineValuesBackground', "Color for the debug inline value background.")); +export const debugInlineBackground = registerColor('editor.inlineValuesBackground', '#ffc80033', nls.localize('editor.inlineValuesBackground', "Color for the debug inline value background.")); class InlineSegment { constructor(public column: number, public text: string) { @@ -126,7 +121,7 @@ function replaceWsWithNoBreakWs(str: string): string { return str.replace(/[ \t]/g, strings.noBreakWhitespace); } -function createInlineValueDecorationsInsideRange(expressions: ReadonlyArray, ranges: Range[], model: ITextModel, wordToLineNumbersMap: Map): IModelDeltaDecoration[] { +function createInlineValueDecorationsInsideRange(expressions: ReadonlyArray, ranges: Range[], model: ITextModel, wordToLineNumbersMap: Map) { const nameValueMap = new Map(); for (const expr of expressions) { nameValueMap.set(expr.name, expr.value); @@ -156,17 +151,14 @@ function createInlineValueDecorationsInsideRange(expressions: ReadonlyArray { - const contentText = names.sort((first, second) => { + return [...lineToNamesMap].map(([line, names]) => ({ + line, + variables: names.sort((first, second) => { const content = model.getLineContent(line); return content.indexOf(first) - content.indexOf(second); - }).map(name => `${name} = ${nameValueMap.get(name)}`).join(', '); - decorations.push(...createInlineValueDecoration(line, contentText)); - }); - - return decorations; + }).map(name => ({ name, value: nameValueMap.get(name)! })) + })); } function getWordToLineNumbersMap(model: ITextModel, lineNumber: number, result: Map) { @@ -208,7 +200,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { private toDispose: IDisposable[]; private hoverWidget: DebugHoverWidget; - private hoverPosition: Position | null = null; + private hoverPosition?: { position: Position; event: IMouseEvent }; private mouseDown = false; private exceptionWidgetVisible: IContextKey; private gutterIsHovered = false; @@ -341,7 +333,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { if (debugHoverWasVisible && this.hoverPosition) { // If the debug hover was visible immediately show the editor hover for the alt transition to be smooth - this.showEditorHover(this.hoverPosition, false); + this.showEditorHover(this.hoverPosition.position, false); } const onKeyUp = new DomEmitter(ownerDocument, 'keyup'); @@ -361,14 +353,14 @@ export class DebugEditorContribution implements IDebugEditorContribution { }); } - async showHover(position: Position, focus: boolean): Promise { + async showHover(position: Position, focus: boolean, mouseEvent?: IMouseEvent): Promise { // normally will already be set in `showHoverScheduler`, but public callers may hit this directly: this.preventDefaultEditorHover(); const sf = this.debugService.getViewModel().focusedStackFrame; const model = this.editor.getModel(); if (sf && model && this.uriIdentityService.extUri.isEqual(sf.source.uri, model.uri)) { - const result = await this.hoverWidget.showAt(position, focus); + const result = await this.hoverWidget.showAt(position, focus, mouseEvent); if (result === ShowDebugHoverResult.NOT_AVAILABLE) { // When no expression available fallback to editor hover this.showEditorHover(position, focus); @@ -438,7 +430,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { private get showHoverScheduler() { const scheduler = new RunOnceScheduler(() => { if (this.hoverPosition && !this.altPressed) { - this.showHover(this.hoverPosition, false); + this.showHover(this.hoverPosition.position, false, this.hoverPosition.event); } }, this.hoverDelay); this.toDispose.push(scheduler); @@ -493,8 +485,8 @@ export class DebugEditorContribution implements IDebugEditorContribution { } if (target.type === MouseTargetType.CONTENT_TEXT) { - if (target.position && !Position.equals(target.position, this.hoverPosition)) { - this.hoverPosition = target.position; + if (target.position && !Position.equals(target.position, this.hoverPosition?.position || null) && !this.hoverWidget.isInSafeTriangle(mouseEvent.event.posx, mouseEvent.event.posy)) { + this.hoverPosition = { position: target.position, event: mouseEvent.event }; // Disable the editor hover during the request to avoid flickering this.preventDefaultEditorHover(); this.showHoverScheduler.schedule(this.hoverDelay); @@ -782,10 +774,15 @@ export class DebugEditorContribution implements IDebugEditorContribution { // old "one-size-fits-all" strategy const scopes = await stackFrame.getMostSpecificScopes(stackFrame.range); - // Get all top level variables in the scope chain - const decorationsPerScope = await Promise.all(scopes.map(async scope => { - const variables = await scope.getChildren(); + const scopesWithVariables = await Promise.all(scopes.map(async scope => + ({ scope, variables: await scope.getChildren() }))); + + // Map of inline values per line that's populated in scope order, from + // narrowest to widest. This is done to avoid duplicating values if + // they appear in multiple scopes or are shadowed (#129770, #217326) + const valuesPerLine = new Map>(); + for (const { scope, variables } of scopesWithVariables) { let scopeRange = new Range(0, 0, stackFrame.range.startLineNumber, stackFrame.range.startColumn); if (scope.range) { scopeRange = scopeRange.setStartPosition(scope.range.startLineNumber, scope.range.startColumn); @@ -797,12 +794,25 @@ export class DebugEditorContribution implements IDebugEditorContribution { this._wordToLineNumbersMap.ensureRangePopulated(range); } - return createInlineValueDecorationsInsideRange(variables, ownRanges, model, this._wordToLineNumbersMap.value); - })); + const mapped = createInlineValueDecorationsInsideRange(variables, ownRanges, model, this._wordToLineNumbersMap.value); + for (const { line, variables } of mapped) { + let values = valuesPerLine.get(line); + if (!values) { + values = new Map(); + valuesPerLine.set(line, values); + } + + for (const { name, value } of variables) { + if (!values.has(name)) { + values.set(name, value); + } + } + } + } - allDecorations = distinct(decorationsPerScope.flat(), - // Deduplicate decorations since same variable can appear in multiple scopes, leading to duplicated decorations #129770 - decoration => `${decoration.range.startLineNumber}:${decoration?.options.after?.content}`); + allDecorations = [...valuesPerLine.entries()].flatMap(([line, values]) => + createInlineValueDecoration(line, [...values].map(([n, v]) => `${n} = ${v}`).join(', ')) + ); } if (cts.token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index b534c3fba67..3b2bf385639 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -5,6 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -83,6 +84,7 @@ export class DebugHoverWidget implements IContentWidget { readonly allowEditorOverflow = true; private _isVisible: boolean; + private safeTriangle?: dom.SafeTriangle; private showCancellationSource?: CancellationTokenSource; private domNode!: HTMLElement; private tree!: AsyncDataTree; @@ -228,7 +230,15 @@ export class DebugHoverWidget implements IContentWidget { return this.domNode; } - async showAt(position: Position, focus: boolean): Promise { + /** + * Gets whether the given coordinates are in the safe triangle formed from + * the position at which the hover was initiated. + */ + isInSafeTriangle(x: number, y: number) { + return this._isVisible && !!this.safeTriangle?.contains(x, y); + } + + async showAt(position: Position, focus: boolean, mouseEvent?: IMouseEvent): Promise { this.showCancellationSource?.cancel(); const cancellationSource = this.showCancellationSource = new CancellationTokenSource(); const session = this.debugService.getViewModel().focusedSession; @@ -269,7 +279,7 @@ export class DebugHoverWidget implements IContentWidget { options: DebugHoverWidget._HOVER_HIGHLIGHT_DECORATION_OPTIONS }]); - return this.doShow(result.range.getStartPosition(), expression, focus); + return this.doShow(result.range.getStartPosition(), expression, focus, mouseEvent); } private static readonly _HOVER_HIGHLIGHT_DECORATION_OPTIONS = ModelDecorationOptions.register({ @@ -277,7 +287,7 @@ export class DebugHoverWidget implements IContentWidget { className: 'hoverHighlight' }); - private async doShow(position: Position, expression: IExpression, focus: boolean, forceValueHover = false): Promise { + private async doShow(position: Position, expression: IExpression, focus: boolean, mouseEvent: IMouseEvent | undefined): Promise { if (!this.domNode) { this.create(); } @@ -285,7 +295,7 @@ export class DebugHoverWidget implements IContentWidget { this.showAtPosition = position; this._isVisible = true; - if (!expression.hasChildren || forceValueHover) { + if (!expression.hasChildren) { this.complexValueContainer.hidden = true; this.valueContainer.hidden = false; renderExpressionValue(expression, this.valueContainer, { @@ -312,6 +322,7 @@ export class DebugHoverWidget implements IContentWidget { this.tree.scrollTop = 0; this.tree.scrollLeft = 0; this.complexValueContainer.hidden = false; + this.safeTriangle = mouseEvent && new dom.SafeTriangle(mouseEvent.posx, mouseEvent.posy, this.domNode); if (focus) { this.editor.render(); @@ -440,8 +451,10 @@ interface IDebugHoverComputeResult { } class DebugHoverComputer { - private _currentRange: Range | undefined; - private _currentExpression: string | undefined; + private _current?: { + range: Range; + expression: string; + }; constructor( private editor: ICodeEditor, @@ -463,30 +476,35 @@ class DebugHoverComputer { } const { range, matchingExpression } = result; - const rangeChanged = this._currentRange ? - !this._currentRange.equalsRange(range) : - true; - this._currentExpression = matchingExpression; - this._currentRange = Range.lift(range); - return { rangeChanged, range: this._currentRange }; + const rangeChanged = !this._current?.range.equalsRange(range); + this._current = { expression: matchingExpression, range: Range.lift(range) }; + return { rangeChanged, range: this._current.range }; } async evaluate(session: IDebugSession): Promise { - if (!this._currentExpression) { + if (!this._current) { this.logService.error('No expression to evaluate'); return; } + const textModel = this.editor.getModel(); + const debugSource = textModel && session.getSourceForUri(textModel?.uri); + if (session.capabilities.supportsEvaluateForHovers) { - const expression = new Expression(this._currentExpression); - await expression.evaluate(session, this.debugService.getViewModel().focusedStackFrame, 'hover'); + const expression = new Expression(this._current.expression); + await expression.evaluate(session, this.debugService.getViewModel().focusedStackFrame, 'hover', undefined, debugSource ? { + line: this._current.range.startLineNumber, + column: this._current.range.startColumn, + source: debugSource.raw, + } : undefined); return expression; } else { const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; if (focusedStackFrame) { return await findExpressionInStackFrame( focusedStackFrame, - coalesce(this._currentExpression.split('.').map(word => word.trim()))); + coalesce(this._current.expression.split('.').map(word => word.trim())) + ); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index d09eada1d2a..8c52537a0ae 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -51,6 +51,7 @@ import { ViewModel } from 'vs/workbench/contrib/debug/common/debugViewModel'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -112,6 +113,7 @@ export class DebugService implements IDebugService { @IQuickInputService private readonly quickInputService: IQuickInputService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @ITestService private readonly testService: ITestService, ) { this.breakpointsToSendOnResourceSaved = new Set(); @@ -202,8 +204,8 @@ export class DebugService implements IDebugService { this.disposables.add(extensionService.onWillStop(evt => { evt.veto( - this.stopSession(undefined).then(() => false), - nls.localize('stoppingDebug', 'Stopping debug sessions...'), + this.model.getSessions().length > 0, + nls.localize('active debug session', 'A debug session is still running.'), ); })); @@ -839,6 +841,21 @@ export class DebugService implements IDebugService { } }; + // For debug sessions spawned by test runs, cancel the test run and stop + // the session, then start the test run again; tests have no notion of restarts. + if (session.correlatedTestRun) { + if (!session.correlatedTestRun.completedAt) { + this.testService.cancelTestRun(session.correlatedTestRun.id); + await Event.toPromise(session.correlatedTestRun.onComplete); + // todo@connor4312 is there any reason to wait for the debug session to + // terminate? I don't think so, test extension should already handle any + // state conflicts... + } + + this.testService.runResolvedTests(session.correlatedTestRun.request); + return; + } + if (session.capabilities.supportsRestartRequest) { const taskResult = await runTasks(); if (taskResult === TaskRunResult.Success) { diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index e2d54e9a296..79c3cc8c123 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -42,6 +42,9 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { getActiveWindow } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; import { isDefined } from 'vs/base/common/types'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500; @@ -66,6 +69,11 @@ export class DebugSession implements IDebugSession, IDisposable { private stoppedDetails: IRawStoppedDetails[] = []; private readonly statusQueue = this.rawListeners.add(new ThreadStatusScheduler()); + /** Test run this debug session was spawned by */ + public readonly correlatedTestRun?: LiveTestResult; + /** Whether we terminated the correlated run yet. Used so a 2nd terminate request goes through to the underlying session. */ + private didTerminateTestRun?: boolean; + private readonly _onDidChangeState = new Emitter(); private readonly _onDidEndAdapter = new Emitter(); @@ -106,7 +114,9 @@ export class DebugSession implements IDebugSession, IDisposable { @IInstantiationService private readonly instantiationService: IInstantiationService, @ICustomEndpointTelemetryService private readonly customEndpointTelemetryService: ICustomEndpointTelemetryService, @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @ITestService private readonly testService: ITestService, + @ITestResultService testResultService: ITestResultService, ) { this._options = options || {}; this.parentSession = this._options.parentSession; @@ -126,6 +136,16 @@ export class DebugSession implements IDebugSession, IDisposable { })); } + // Cast here, it's not possible to reference a hydrated result in this code path. + this.correlatedTestRun = options?.testRun + ? (testResultService.getResult(options.testRun.runId) as LiveTestResult) + : this.parentSession?.correlatedTestRun; + + if (this.correlatedTestRun) { + // Listen to the test completing because the user might have taken the cancel action rather than stopping the session. + toDispose.add(this.correlatedTestRun.onComplete(() => this.terminate())); + } + const compoundRoot = this._options.compoundRoot; if (compoundRoot) { toDispose.add(compoundRoot.onDidSessionStop(() => this.terminate())); @@ -387,6 +407,9 @@ export class DebugSession implements IDebugSession, IDisposable { this.cancelAllRequests(); if (this._options.lifecycleManagedByParent && this.parentSession) { await this.parentSession.terminate(restart); + } else if (this.correlatedTestRun && !this.correlatedTestRun.completedAt && !this.didTerminateTestRun) { + this.didTerminateTestRun = true; + this.testService.cancelTestRun(this.correlatedTestRun.id); } else if (this.raw) { if (this.raw.capabilities.supportsTerminateRequest && this._configuration.resolved.request === 'launch') { await this.raw.terminate(restart); @@ -662,12 +685,12 @@ export class DebugSession implements IDebugSession, IDisposable { return this.raw.variables({ variablesReference, filter, start, count }, token); } - evaluate(expression: string, frameId: number, context?: string): Promise { + evaluate(expression: string, frameId: number, context?: string, location?: { line: number; column: number; source: DebugProtocol.Source }): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'evaluate')); } - return this.raw.evaluate({ expression, frameId, context }); + return this.raw.evaluate({ expression, frameId, context, line: location?.line, column: location?.column, source: location?.source }); } async restartFrame(frameId: number, threadId: number): Promise { @@ -1498,8 +1521,8 @@ export class DebugSession implements IDebugSession, IDisposable { this.repl.removeReplExpressions(); } - async addReplExpression(stackFrame: IStackFrame | undefined, name: string): Promise { - await this.repl.addReplExpression(this, stackFrame, name); + async addReplExpression(stackFrame: IStackFrame | undefined, expression: string): Promise { + await this.repl.addReplExpression(this, stackFrame, expression); // Evaluate all watch expressions and fetch variables again since repl evaluation might have changed some. this.debugService.getViewModel().updateViews(); } diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index add88e75098..4bae5574e46 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -10,7 +10,7 @@ import { Action, IAction, IRunEvent, WorkbenchActionExecutedClassification, Work import * as arrays from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as errors from 'vs/base/common/errors'; -import { DisposableStore, dispose, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable, markAsSingleton, MutableDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/debugToolBar'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; @@ -401,7 +401,7 @@ const registerDebugToolBarItem = (id: string, title: string | ICommandActionTitl })); }; -MenuRegistry.onDidChangeMenu(e => { +markAsSingleton(MenuRegistry.onDidChangeMenu(e => { // In case the debug toolbar is docked we need to make sure that the docked toolbar has the up to date commands registered #115945 if (e.has(MenuId.DebugToolBar)) { dispose(debugViewTitleItems); @@ -413,7 +413,7 @@ MenuRegistry.onDidChangeMenu(e => { })); } } -}); +})); const CONTEXT_TOOLBAR_COMMAND_CENTER = ContextKeyExpr.equals('config.debug.toolBarLocation', 'commandCenter'); diff --git a/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts b/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts index 1b920801dec..7c9b48503a2 100644 --- a/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts @@ -24,7 +24,7 @@ const $ = dom.$; // theming -const debugExceptionWidgetBorder = registerColor('debugExceptionWidget.border', { dark: '#a31515', light: '#a31515', hcDark: '#a31515', hcLight: '#a31515' }, nls.localize('debugExceptionWidgetBorder', 'Exception widget border color.')); +const debugExceptionWidgetBorder = registerColor('debugExceptionWidget.border', '#a31515', nls.localize('debugExceptionWidgetBorder', 'Exception widget border color.')); const debugExceptionWidgetBackground = registerColor('debugExceptionWidget.background', { dark: '#420b0d', light: '#f1dfde', hcDark: '#420b0d', hcLight: '#f1dfde' }, nls.localize('debugExceptionWidgetBackground', 'Exception widget background color.')); export class ExceptionWidget extends ZoneWidget { diff --git a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css index fbe36b38c1a..fc948f97b4a 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css @@ -5,7 +5,7 @@ .monaco-workbench .debug-toolbar { position: absolute; - z-index: 3000; + z-index: 2520; /* Below quick input at 2550, above custom titlebar toolbar at 2500 */ height: 26px; display: flex; padding-left: 7px; diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index 5baf2c48fb2..59696ad5004 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -52,7 +52,7 @@ display: flex; } -.monaco-workbench .repl .repl-tree .output.expression.value-and-source .value { +.monaco-workbench .repl .repl-tree .output.expression.value-and-source .label { margin-right: 4px; } @@ -71,7 +71,8 @@ left: 2px; } -.monaco-workbench .repl .repl-tree .output.expression.value-and-source .source { +.monaco-workbench .repl .repl-tree .output.expression.value-and-source .source, +.monaco-workbench .repl .repl-tree .group .source { margin-left: auto; margin-right: 8px; cursor: pointer; diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 08fc8f4a661..c28c0dbc4d3 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -151,7 +151,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.menu = menuService.createMenu(MenuId.DebugConsoleContext, contextKeyService); this._register(this.menu); - this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); + this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 100); this.filter = new ReplFilter(); this.filter.filterQuery = filterText; this.multiSessionRepl = CONTEXT_MULTI_SESSION_REPL.bindTo(contextKeyService); @@ -472,6 +472,15 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { } } + sendReplInput(input: string): void { + const session = this.tree?.getInput(); + if (session && !this.isReadonly) { + session.addReplExpression(this.debugService.getViewModel().focusedStackFrame, input); + revealLastElement(this.tree!); + this.history.add(input); + } + } + getVisibleContent(): string { let text = ''; if (this.model && this.tree) { @@ -687,7 +696,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { }; CONTEXT_IN_DEBUG_REPL.bindTo(this.scopedContextKeyService).set(true); - this.scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); + this.scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); const options = getSimpleEditorOptions(this.configurationService); options.readOnly = true; options.suggest = { showStatusBar: true }; diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index bae5c31696c..50d81a8f041 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -6,20 +6,25 @@ import * as dom from 'vs/base/browser/dom'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { IManagedHover } from 'vs/base/browser/ui/hover/hover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/path'; import severity from 'vs/base/common/severity'; +import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ThemeIcon } from 'vs/base/common/themables'; import { AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderExpressionValue, renderVariable } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling'; import { debugConsoleEvaluationInput } from 'vs/workbench/contrib/debug/browser/debugIcons'; @@ -28,9 +33,6 @@ import { IDebugConfiguration, IDebugService, IDebugSession, IExpression, IExpres import { Variable } from 'vs/workbench/contrib/debug/common/debugModel'; import { RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult, ReplGroup, ReplOutputElement, ReplVariableElement } from 'vs/workbench/contrib/debug/common/replModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; const $ = dom.$; @@ -40,6 +42,7 @@ interface IReplEvaluationInputTemplateData { interface IReplGroupTemplateData { label: HTMLElement; + source: SourceWidget; } interface IReplEvaluationResultTemplateData { @@ -51,9 +54,8 @@ interface IOutputReplElementTemplateData { count: CountBadge; countContainer: HTMLElement; value: HTMLElement; - source: HTMLElement; + source: SourceWidget; getReplElementSource(): IReplElementSource | undefined; - toDispose: IDisposable[]; elementListener: IDisposable; } @@ -94,7 +96,8 @@ export class ReplGroupRenderer implements ITreeRenderer, _index: number, templateData: IReplGroupTemplateData): void { @@ -111,10 +117,11 @@ export class ReplGroupRenderer implements ITreeRenderer { - e.preventDefault(); - e.stopPropagation(); - const source = data.getReplElementSource(); - if (source) { - source.source.openInEditor(this.editorService, { - startLineNumber: source.lineNumber, - startColumn: source.column, - endLineNumber: source.lineNumber, - endColumn: source.column - }); - } - })); + data.value = dom.append(expression, $('span.value.label')); + data.source = this.instaService.createInstance(SourceWidget, expression); return data; } @@ -204,8 +195,7 @@ export class ReplOutputElementRenderer implements ITreeRenderer element.sourceData; } @@ -219,7 +209,7 @@ export class ReplOutputElementRenderer implements ITreeRenderer, _index: number, templateData: IOutputReplElementTemplateData): void { @@ -247,6 +237,7 @@ export class ReplVariablesRenderer extends AbstractExpressionsRenderer, _index: number, data: IExpressionTemplateData): void { const element = node.element; + data.elementDisposable.clear(); super.renderExpressionElement(element instanceof ReplVariableElement ? element.expression : element, node, data); } @@ -431,3 +422,39 @@ export class ReplAccessibilityProvider implements IListAccessibilityProvider { + e.preventDefault(); + e.stopPropagation(); + if (this.source) { + this.source.source.openInEditor(editorService, { + startLineNumber: this.source.lineNumber, + startColumn: this.source.column, + endLineNumber: this.source.lineNumber, + endColumn: this.source.column + }); + } + })); + + } + + public setSource(source?: IReplElementSource) { + this.source = source; + this.el.textContent = source ? `${basename(source.source.name)}:${source.lineNumber}` : ''; + + this.hover ??= this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.el, '')); + this.hover.update(source ? `${this.labelService.getUriLabel(source.source.uri)}:${source.lineNumber}` : ''); + } +} diff --git a/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts b/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts index 178711402de..9f551fd3ad6 100644 --- a/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts +++ b/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts @@ -4,14 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { ColorTransformType, asCssVariable, asCssVariableName, registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { asCssVariable, asCssVariableName, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; // --- Start Positron --- import { inputBackground } from 'vs/platform/theme/common/colorRegistry'; // eslint-disable-line no-duplicate-imports // --- End Positron --- import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IDebugService, State, IDebugSession, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { STATUS_BAR_FOREGROUND, STATUS_BAR_BORDER, COMMAND_CENTER_BACKGROUND } from 'vs/workbench/common/theme'; +import { IWorkspaceContextService } from +'vs/platform/workspace/common/workspace'; import { STATUS_BAR_FOREGROUND, + STATUS_BAR_BORDER, COMMAND_CENTER_BACKGROUND } from + 'vs/workbench/common/theme'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { IStatusbarService } from 'vs/workbench/services/statusbar/browser/statusbar'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -38,21 +40,11 @@ export const STATUS_BAR_DEBUGGING_FOREGROUND = registerColor('statusBar.debuggin hcLight: '#FFFFFF' }, localize('statusBarDebuggingForeground', "Status bar foreground color when a program is being debugged. The status bar is shown in the bottom of the window")); -export const STATUS_BAR_DEBUGGING_BORDER = registerColor('statusBar.debuggingBorder', { - dark: STATUS_BAR_BORDER, - light: STATUS_BAR_BORDER, - hcDark: STATUS_BAR_BORDER, - hcLight: STATUS_BAR_BORDER -}, localize('statusBarDebuggingBorder', "Status bar border color separating to the sidebar and editor when a program is being debugged. The status bar is shown in the bottom of the window")); +export const STATUS_BAR_DEBUGGING_BORDER = registerColor('statusBar.debuggingBorder', STATUS_BAR_BORDER, localize('statusBarDebuggingBorder', "Status bar border color separating to the sidebar and editor when a program is being debugged. The status bar is shown in the bottom of the window")); export const COMMAND_CENTER_DEBUGGING_BACKGROUND = registerColor( 'commandCenter.debuggingBackground', - { - dark: { value: STATUS_BAR_DEBUGGING_BACKGROUND, op: ColorTransformType.Transparent, factor: 0.258 }, - hcDark: { value: STATUS_BAR_DEBUGGING_BACKGROUND, op: ColorTransformType.Transparent, factor: 0.258 }, - light: { value: STATUS_BAR_DEBUGGING_BACKGROUND, op: ColorTransformType.Transparent, factor: 0.258 }, - hcLight: { value: STATUS_BAR_DEBUGGING_BACKGROUND, op: ColorTransformType.Transparent, factor: 0.258 } - }, + transparent(STATUS_BAR_DEBUGGING_BACKGROUND, 0.258), localize('commandCenter-activeBackground', "Command center background color when a program is being debugged"), true ); @@ -93,7 +85,7 @@ export class StatusBarColorProvider implements IWorkbenchContribution { if (e.affectsConfiguration('debug.enableStatusBarColor') || e.affectsConfiguration('debug.toolBarLocation')) { this.update(); } - }, this.disposables); + }, undefined, this.disposables); this.update(); } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 5dc2a66b073..78caf7f02e6 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -41,7 +41,7 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderExpressionValue, renderVariable, renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { ADD_TO_WATCH_ID, ADD_TO_WATCH_LABEL, COPY_EVALUATE_PATH_ID, COPY_EVALUATE_PATH_LABEL, COPY_VALUE_ID, COPY_VALUE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; -import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DataBreakpointSetType, DebugVisualizationType, IDataBreakpointInfoResponse, IDebugService, IExpression, IScope, IStackFrame, IViewModel, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DataBreakpointSetType, DebugVisualizationType, IDataBreakpointInfoResponse, IDebugConfiguration, IDebugService, IExpression, IScope, IStackFrame, IViewModel, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { getContextForVariable } from 'vs/workbench/contrib/debug/common/debugContext'; import { ErrorScope, Expression, Scope, StackFrame, Variable, VisualizedExpression, getUriForDebugMemory } from 'vs/workbench/contrib/debug/common/debugModel'; import { DebugVisualizer, IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; @@ -455,6 +455,7 @@ export class VisualizedVariableRenderer extends AbstractExpressionsRenderer { } public override renderElement(node: ITreeNode, index: number, data: IExpressionTemplateData): void { + data.elementDisposable.clear(); super.renderExpressionElement(node.element, node, data); } @@ -531,6 +532,7 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { @IDebugService debugService: IDebugService, @IContextViewService contextViewService: IContextViewService, @IHoverService hoverService: IHoverService, + @IConfigurationService private configurationService: IConfigurationService, ) { super(debugService, contextViewService, hoverService); } @@ -540,10 +542,17 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { } protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void { - renderVariable(data.elementDisposable, this.commandService, this.hoverService, expression as Variable, data, true, highlights, this.linkDetector); + const showType = this.configurationService.getValue('debug').showVariableTypes; + renderVariable(data.elementDisposable, this.commandService, this.hoverService, expression as Variable, data, true, highlights, this.linkDetector, showType); } public override renderElement(node: ITreeNode, index: number, data: IExpressionTemplateData): void { + data.elementDisposable.clear(); + data.elementDisposable.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('debug.showVariableTypes')) { + super.renderExpressionElement(node.element, node, data); + } + })); super.renderExpressionElement(node.element, node, data); } diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 7369a367607..07f6986534d 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -34,7 +34,7 @@ import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionT import { watchExpressionsAdd, watchExpressionsRemoveAll } from 'vs/workbench/contrib/debug/browser/debugIcons'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { VariablesRenderer, VisualizedVariableRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; -import { CONTEXT_CAN_VIEW_MEMORY, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_WATCH_ITEM_TYPE, IDebugService, IExpression, WATCH_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_CAN_VIEW_MEMORY, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_WATCH_ITEM_TYPE, IDebugConfiguration, IDebugService, IExpression, WATCH_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { Expression, Variable, VisualizedExpression } from 'vs/workbench/contrib/debug/common/debugModel'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; @@ -157,7 +157,7 @@ export class WatchExpressionsView extends ViewPane { let horizontalScrolling: boolean | undefined; this._register(this.debugService.getViewModel().onDidSelectExpression(e => { const expression = e?.expression; - if (expression && this.tree.hasElement(expression)) { + if (expression && this.tree.hasNode(expression)) { horizontalScrolling = this.tree.options.horizontalScrolling; if (horizontalScrolling) { this.tree.updateOptions({ horizontalScrolling: false }); @@ -274,7 +274,7 @@ class WatchExpressionsDataSource extends AbstractExpressionDataSource, index: number, data: IExpressionTemplateData): void { + data.elementDisposable.clear(); + data.elementDisposable.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('debug.showVariableTypes')) { + super.renderExpressionElement(node.element, node, data); + } + })); super.renderExpressionElement(node.element, node, data); } protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void { - const text = typeof expression.value === 'string' ? `${expression.name}:` : expression.name; + let text: string; + data.type.textContent = ''; + const showType = this.configurationService.getValue('debug').showVariableTypes; + if (showType && expression.type) { + text = typeof expression.value === 'string' ? `${expression.name}: ` : expression.name; + //render type + data.type.textContent = expression.type + ' ='; + } else { + text = typeof expression.value === 'string' ? `${expression.name} =` : expression.name; + } + let title: string; if (expression.type) { - title = expression.type === expression.value ? - expression.type : - `${expression.type}: ${expression.value}`; + if (showType) { + title = `${expression.name}`; + } else { + title = expression.type === expression.value ? + expression.type : + `${expression.type}`; + } } else { title = expression.value; } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index e48607b9eda..41f1a55b557 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -27,6 +27,7 @@ import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompou import { IDataBreakpointOptions, IFunctionBreakpointOptions, IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { ITaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; +import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export const VIEWLET_ID = 'workbench.view.debug'; @@ -50,9 +51,9 @@ export const CONTEXT_IN_DEBUG_REPL = new RawContextKey('inDebugRepl', f export const CONTEXT_BREAKPOINT_WIDGET_VISIBLE = new RawContextKey('breakpointWidgetVisible', false, { type: 'boolean', description: nls.localize('breakpointWidgetVisibile', "True when breakpoint editor zone widget is visible, false otherwise.") }); export const CONTEXT_IN_BREAKPOINT_WIDGET = new RawContextKey('inBreakpointWidget', false, { type: 'boolean', description: nls.localize('inBreakpointWidget', "True when focus is in the breakpoint editor zone widget, false otherwise.") }); export const CONTEXT_BREAKPOINTS_FOCUSED = new RawContextKey('breakpointsFocused', true, { type: 'boolean', description: nls.localize('breakpointsFocused', "True when the BREAKPOINTS view is focused, false otherwise.") }); -export const CONTEXT_WATCH_EXPRESSIONS_FOCUSED = new RawContextKey('watchExpressionsFocused', true, { type: 'boolean', description: nls.localize('watchExpressionsFocused', "True when the WATCH view is focused, false otherwsie.") }); +export const CONTEXT_WATCH_EXPRESSIONS_FOCUSED = new RawContextKey('watchExpressionsFocused', true, { type: 'boolean', description: nls.localize('watchExpressionsFocused', "True when the WATCH view is focused, false otherwise.") }); export const CONTEXT_WATCH_EXPRESSIONS_EXIST = new RawContextKey('watchExpressionsExist', false, { type: 'boolean', description: nls.localize('watchExpressionsExist', "True when at least one watch expression exists, false otherwise.") }); -export const CONTEXT_VARIABLES_FOCUSED = new RawContextKey('variablesFocused', true, { type: 'boolean', description: nls.localize('variablesFocused', "True when the VARIABLES views is focused, false otherwsie") }); +export const CONTEXT_VARIABLES_FOCUSED = new RawContextKey('variablesFocused', true, { type: 'boolean', description: nls.localize('variablesFocused', "True when the VARIABLES views is focused, false otherwise") }); export const CONTEXT_EXPRESSION_SELECTED = new RawContextKey('expressionSelected', false, { type: 'boolean', description: nls.localize('expressionSelected', "True when an expression input box is open in either the WATCH or the VARIABLES view, false otherwise.") }); export const CONTEXT_BREAKPOINT_INPUT_FOCUSED = new RawContextKey('breakpointInputFocused', false, { type: 'boolean', description: nls.localize('breakpointInputFocused', "True when the input box has focus in the BREAKPOINTS view.") }); export const CONTEXT_CALLSTACK_ITEM_TYPE = new RawContextKey('callStackItemType', undefined, { type: 'string', description: nls.localize('callStackItemType', "Represents the item type of the focused element in the CALL STACK view. For example: 'session', 'thread', 'stackFrame'") }); @@ -71,7 +72,7 @@ export const CONTEXT_FOCUSED_SESSION_IS_ATTACH = new RawContextKey('foc export const CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG = new RawContextKey('focusedSessionIsNoDebug', false, { type: 'boolean', description: nls.localize('focusedSessionIsNoDebug', "True when the focused session is run without debugging.") }); export const CONTEXT_STEP_BACK_SUPPORTED = new RawContextKey('stepBackSupported', false, { type: 'boolean', description: nls.localize('stepBackSupported', "True when the focused session supports 'stepBack' requests.") }); export const CONTEXT_RESTART_FRAME_SUPPORTED = new RawContextKey('restartFrameSupported', false, { type: 'boolean', description: nls.localize('restartFrameSupported', "True when the focused session supports 'restartFrame' requests.") }); -export const CONTEXT_STACK_FRAME_SUPPORTS_RESTART = new RawContextKey('stackFrameSupportsRestart', false, { type: 'boolean', description: nls.localize('stackFrameSupportsRestart', "True when the focused stack frame suppots 'restartFrame'.") }); +export const CONTEXT_STACK_FRAME_SUPPORTS_RESTART = new RawContextKey('stackFrameSupportsRestart', false, { type: 'boolean', description: nls.localize('stackFrameSupportsRestart', "True when the focused stack frame supports 'restartFrame'.") }); export const CONTEXT_JUMP_TO_CURSOR_SUPPORTED = new RawContextKey('jumpToCursorSupported', false, { type: 'boolean', description: nls.localize('jumpToCursorSupported', "True when the focused session supports 'jumpToCursor' request.") }); export const CONTEXT_STEP_INTO_TARGETS_SUPPORTED = new RawContextKey('stepIntoTargetsSupported', false, { type: 'boolean', description: nls.localize('stepIntoTargetsSupported', "True when the focused session supports 'stepIntoTargets' request.") }); export const CONTEXT_BREAKPOINTS_EXIST = new RawContextKey('breakpointsExist', false, { type: 'boolean', description: nls.localize('breakpointsExist', "True when at least one breakpoint exists.") }); @@ -219,6 +220,11 @@ export interface LoadedSourceEvent { export type IDebugSessionReplMode = 'separate' | 'mergeWithParent'; +export interface IDebugTestRunReference { + runId: string; + taskId: string; +} + export interface IDebugSessionOptions { noDebug?: boolean; parentSession?: IDebugSession; @@ -231,6 +237,11 @@ export interface IDebugSessionOptions { suppressDebugToolbar?: boolean; suppressDebugStatusbar?: boolean; suppressDebugView?: boolean; + /** + * Set if the debug session is correlated with a test run. Stopping/restarting + * the session will instead stop/restart the test run. + */ + testRun?: IDebugTestRunReference; } export interface IDataBreakpointInfoResponse { @@ -335,6 +346,12 @@ export interface INewReplElementData { source?: IReplElementSource; } +export interface IDebugEvaluatePosition { + line: number; + column: number; + source: DebugProtocol.Source; +} + export interface IDebugSession extends ITreeElement { @@ -353,6 +370,8 @@ export interface IDebugSession extends ITreeElement { readonly suppressDebugStatusbar: boolean; readonly suppressDebugView: boolean; readonly lifecycleManagedByParent: boolean; + /** Test run this debug session was spawned by */ + readonly correlatedTestRun?: LiveTestResult; setSubId(subId: string | undefined): void; @@ -418,7 +437,7 @@ export interface IDebugSession extends ITreeElement { exceptionInfo(threadId: number): Promise; scopes(frameId: number, threadId: number): Promise; variables(variablesReference: number, threadId: number | undefined, filter: 'indexed' | 'named' | undefined, start: number | undefined, count: number | undefined): Promise; - evaluate(expression: string, frameId?: number, context?: string): Promise; + evaluate(expression: string, frameId?: number, context?: string, location?: IDebugEvaluatePosition): Promise; customRequest(request: string, args: any): Promise; cancel(progressId: string): Promise; disassemble(memoryReference: string, offset: number, instructionOffset: number, instructionCount: number): Promise; @@ -534,7 +553,8 @@ export interface IStackFrame extends ITreeElement { } export function isFrameDeemphasized(frame: IStackFrame): boolean { - return frame.source.presentationHint === 'deemphasize' || frame.presentationHint === 'deemphasize' || frame.presentationHint === 'subtle'; + const hint = frame.presentationHint ?? frame.source.presentationHint; + return hint === 'deemphasize' || hint === 'subtle'; } export interface IEnablement extends ITreeElement { @@ -774,6 +794,7 @@ export interface IDebugConfiguration { }; autoExpandLazyVariables: boolean; enableStatusBarColor: boolean; + showVariableTypes: boolean; } export interface IGlobalConfig { diff --git a/src/vs/workbench/contrib/debug/common/debugLifecycle.ts b/src/vs/workbench/contrib/debug/common/debugLifecycle.ts index 838140a91d0..68420965735 100644 --- a/src/vs/workbench/contrib/debug/common/debugLifecycle.ts +++ b/src/vs/workbench/contrib/debug/common/debugLifecycle.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IDisposable } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -11,13 +12,15 @@ import { IDebugConfiguration, IDebugService } from 'vs/workbench/contrib/debug/c import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; export class DebugLifecycle implements IWorkbenchContribution { + private disposable: IDisposable; + constructor( @ILifecycleService lifecycleService: ILifecycleService, @IDebugService private readonly debugService: IDebugService, @IConfigurationService private readonly configurationService: IConfigurationService, @IDialogService private readonly dialogService: IDialogService, ) { - lifecycleService.onBeforeShutdown(async e => e.veto(this.shouldVetoShutdown(e.reason), 'veto.debug')); + this.disposable = lifecycleService.onBeforeShutdown(async e => e.veto(this.shouldVetoShutdown(e.reason), 'veto.debug')); } private shouldVetoShutdown(_reason: ShutdownReason): boolean | Promise { @@ -34,6 +37,10 @@ export class DebugLifecycle implements IWorkbenchContribution { return this.showWindowCloseConfirmation(rootSessions.length); } + public dispose() { + return this.disposable.dispose(); + } + private async showWindowCloseConfirmation(numSessions: number): Promise { let message: string; if (numSessions === 1) { diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 13f7d1d902d..f9c67ec8958 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -22,7 +22,7 @@ import * as nls from 'vs/nls'; import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IEditorPane } from 'vs/workbench/common/editor'; -import { DEBUG_MEMORY_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State, isFrameDeemphasized } from 'vs/workbench/contrib/debug/common/debug'; +import { DEBUG_MEMORY_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugEvaluatePosition, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State, isFrameDeemphasized } from 'vs/workbench/contrib/debug/common/debug'; import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; @@ -198,7 +198,9 @@ export class ExpressionContainer implements IExpressionContainer { session: IDebugSession | undefined, stackFrame: IStackFrame | undefined, context: string, - keepLazyVars = false): Promise { + keepLazyVars = false, + location?: IDebugEvaluatePosition, + ): Promise { if (!session || (!stackFrame && context !== 'repl')) { this.value = context === 'repl' ? nls.localize('startDebugFirst', "Please start a debug session to evaluate expressions") : Expression.DEFAULT_VALUE; @@ -208,7 +210,7 @@ export class ExpressionContainer implements IExpressionContainer { this.session = session; try { - const response = await session.evaluate(expression, stackFrame ? stackFrame.frameId : undefined, context); + const response = await session.evaluate(expression, stackFrame ? stackFrame.frameId : undefined, context, location); if (response && response.body) { this.value = response.body.result || ''; @@ -306,8 +308,8 @@ export class Expression extends ExpressionContainer implements IExpression { } } - async evaluate(session: IDebugSession | undefined, stackFrame: IStackFrame | undefined, context: string, keepLazyVars?: boolean): Promise { - this.available = await this.evaluateExpression(this.name, session, stackFrame, context, keepLazyVars); + async evaluate(session: IDebugSession | undefined, stackFrame: IStackFrame | undefined, context: string, keepLazyVars?: boolean, location?: IDebugEvaluatePosition): Promise { + this.available = await this.evaluateExpression(this.name, session, stackFrame, context, keepLazyVars, location); } override toString(): string { @@ -540,6 +542,8 @@ export class StackFrame implements IStackFrame { } } +const KEEP_SUBTLE_FRAME_AT_TOP_REASONS: readonly string[] = ['breakpoint', 'step', 'function breakpoint']; + export class Thread implements IThread { private callStack: IStackFrame[]; private staleCallStack: IStackFrame[]; @@ -578,10 +582,11 @@ export class Thread implements IThread { getTopStackFrame(): IStackFrame | undefined { const callStack = this.getCallStack(); + const stopReason = this.stoppedDetails?.reason; // Allow stack frame without source and with instructionReferencePointer as top stack frame when using disassembly view. const firstAvailableStackFrame = callStack.find(sf => !!( - ((this.stoppedDetails?.reason === 'instruction breakpoint' || (this.stoppedDetails?.reason === 'step' && this.lastSteppingGranularity === 'instruction')) && sf.instructionPointerReference) || - (sf.source && sf.source.available && !isFrameDeemphasized(sf)))); + ((stopReason === 'instruction breakpoint' || (stopReason === 'step' && this.lastSteppingGranularity === 'instruction')) && sf.instructionPointerReference) || + (sf.source && sf.source.available && (KEEP_SUBTLE_FRAME_AT_TOP_REASONS.includes(stopReason!) || !isFrameDeemphasized(sf))))); return firstAvailableStackFrame; } @@ -1544,7 +1549,13 @@ export class DebugModel extends Disposable implements IDebugModel { let topCallStack = Promise.resolve(); const wholeCallStack = new Promise((c, e) => { topCallStack = thread.fetchCallStack(1).then(() => { - if (!this.schedulers.has(thread.getId()) && fetchFullStack) { + if (!fetchFullStack) { + c(); + this._onDidChangeCallStack.fire(); + return; + } + + if (!this.schedulers.has(thread.getId())) { const deferred = new DeferredPromise(); this.schedulers.set(thread.getId(), { completeDeferred: deferred, diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index 50eacfd65e2..963e3d553e4 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -1377,6 +1377,15 @@ declare module DebugProtocol { expression: string; /** Evaluate the expression in the scope of this stack frame. If not specified, the expression is evaluated in the global scope. */ frameId?: number; + /** The contextual line where the expression should be evaluated. In the 'hover' context, this should be set to the start of the expression being hovered. */ + line?: number; + /** The contextual column where the expression should be evaluated. This may be provided if `line` is also provided. + + It is measured in UTF-16 code units and the client capability `columnsStartAt1` determines whether it is 0- or 1-based. + */ + column?: number; + /** The contextual source in which the `line` is found. This must be provided if `line` is provided. */ + source?: Source; /** The context in which the evaluate request is used. Values: 'watch': evaluate is called from a watch view context. @@ -2401,7 +2410,7 @@ declare module DebugProtocol { Values: 'source': In `SourceBreakpoint`s 'exception': In exception breakpoints applied in the `ExceptionFilterOptions` - 'data': In data breakpoints requested in the the `DataBreakpointInfo` request + 'data': In data breakpoints requested in the `DataBreakpointInfo` request 'instruction': In `InstructionBreakpoint`s etc. */ diff --git a/src/vs/workbench/contrib/debug/common/debugVisualizers.ts b/src/vs/workbench/contrib/debug/common/debugVisualizers.ts index 45d19aee6f9..47baa50ee41 100644 --- a/src/vs/workbench/contrib/debug/common/debugVisualizers.ts +++ b/src/vs/workbench/contrib/debug/common/debugVisualizers.ts @@ -7,7 +7,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; import { isDefined } from 'vs/base/common/types'; import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ExtensionIdentifier, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { CONTEXT_VARIABLE_NAME, CONTEXT_VARIABLE_TYPE, CONTEXT_VARIABLE_VALUE, MainThreadDebugVisualization, IDebugVisualization, IDebugVisualizationContext, IExpression, IExpressionContainer, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; @@ -250,7 +250,7 @@ export class DebugVisualizerService implements IDebugVisualizerService { return context; } - private processExtensionRegistration(ext: Readonly) { + private processExtensionRegistration(ext: IExtensionDescription) { const viz = ext.contributes?.debugVisualizers; if (!(viz instanceof Array)) { return; diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index 4402ed3f3b5..6556849e5e9 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -269,10 +269,10 @@ export class ReplModel { return this.replElements; } - async addReplExpression(session: IDebugSession, stackFrame: IStackFrame | undefined, name: string): Promise { - this.addReplElement(new ReplEvaluationInput(name)); - const result = new ReplEvaluationResult(name); - await result.evaluateExpression(name, session, stackFrame, 'repl'); + async addReplExpression(session: IDebugSession, stackFrame: IStackFrame | undefined, expression: string): Promise { + this.addReplElement(new ReplEvaluationInput(expression)); + const result = new ReplEvaluationResult(expression); + await result.evaluateExpression(expression, session, stackFrame, 'repl'); this.addReplElement(result); } diff --git a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index a5ba220e380..2f591514d5a 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as dom from 'vs/base/browser/dom'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isWindows } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullCommandService } from 'vs/platform/commands/test/common/nullCommandService'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; import { NullHoverService } from 'vs/platform/hover/test/browser/nullHoverService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { renderExpressionValue, renderVariable, renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; @@ -23,6 +24,66 @@ import { MockSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; const $ = dom.$; +function assertVariable(session: MockSession, scope: Scope, disposables: Pick, linkDetector: LinkDetector, displayType: boolean) { + let variable = new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined, 0, 0, undefined, {}, 'string'); + let expression = $('.'); + let name = $('.'); + let type = $('.'); + let value = $('.'); + const label = new HighlightedLabel(name); + const lazyButton = $('.'); + const store = disposables.add(new DisposableStore()); + renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], undefined, displayType); + + assert.strictEqual(label.element.textContent, 'foo'); + assert.strictEqual(value.textContent, ''); + + variable.value = 'hey'; + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); + assert.strictEqual(value.textContent, 'hey'); + assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); + assert.strictEqual(type.textContent, displayType ? 'string =' : ''); + + variable.value = isWindows ? 'C:\\foo.js:5' : '/foo.js:5'; + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); + assert.ok(value.querySelector('a')); + assert.strictEqual(value.querySelector('a')!.textContent, variable.value); + + variable = new Variable(session, 1, scope, 2, 'console', 'console', '5', 0, 0, undefined, { kind: 'virtual' }); + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); + assert.strictEqual(name.className, 'virtual'); + assert.strictEqual(label.element.textContent, 'console ='); + assert.strictEqual(value.className, 'value number'); + + variable = new Variable(session, 1, scope, 2, 'xpto', 'xpto.xpto', undefined, 0, 0, undefined, {}, 'custom-type'); + renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); + assert.strictEqual(label.element.textContent, 'xpto'); + assert.strictEqual(value.textContent, ''); + variable.value = '2'; + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); + assert.strictEqual(value.textContent, '2'); + assert.strictEqual(label.element.textContent, displayType ? 'xpto: ' : 'xpto ='); + assert.strictEqual(type.textContent, displayType ? 'custom-type =' : ''); + + label.dispose(); +} + suite('Debug - Base Debug View', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let linkDetector: LinkDetector; @@ -33,6 +94,7 @@ suite('Debug - Base Debug View', () => { setup(() => { const instantiationService: TestInstantiationService = workbenchInstantiationService(undefined, disposables); linkDetector = instantiationService.createInstance(LinkDetector); + instantiationService.stub(IHoverService, NullHoverService); }); test('render view tree', () => { @@ -85,47 +147,32 @@ suite('Debug - Base Debug View', () => { test('render variable', () => { const session = new MockSession(); const thread = new Thread(session, 'mockthread', 1); - const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: undefined!, endColumn: undefined! }, 0, true); + const range = { + startLineNumber: 1, + startColumn: 1, + endLineNumber: undefined!, + endColumn: undefined! + }; + const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', range, 0, true); + const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); + + assertVariable(session, scope, disposables, linkDetector, false); + + }); + + test('render variable with display type setting', () => { + const session = new MockSession(); + const thread = new Thread(session, 'mockthread', 1); + const range = { + startLineNumber: 1, + startColumn: 1, + endLineNumber: undefined!, + endColumn: undefined! + }; + const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', range, 0, true); const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); - let variable = new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined, 0, 0, undefined, {}, 'string'); - let expression = $('.'); - let name = $('.'); - let value = $('.'); - const label = new HighlightedLabel(name); - const lazyButton = $('.'); - const store = disposables.add(new DisposableStore()); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, value, label, lazyButton }, false, []); - - assert.strictEqual(label.element.textContent, 'foo'); - assert.strictEqual(value.textContent, ''); - - variable.value = 'hey'; - expression = $('.'); - name = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, value, label, lazyButton }, false, [], linkDetector); - assert.strictEqual(value.textContent, 'hey'); - assert.strictEqual(label.element.textContent, 'foo:'); - - variable.value = isWindows ? 'C:\\foo.js:5' : '/foo.js:5'; - expression = $('.'); - name = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, value, label, lazyButton }, false, [], linkDetector); - assert.ok(value.querySelector('a')); - assert.strictEqual(value.querySelector('a')!.textContent, variable.value); - - variable = new Variable(session, 1, scope, 2, 'console', 'console', '5', 0, 0, undefined, { kind: 'virtual' }); - expression = $('.'); - name = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, value, label, lazyButton }, false, [], linkDetector); - assert.strictEqual(name.className, 'virtual'); - assert.strictEqual(label.element.textContent, 'console:'); - assert.strictEqual(value.className, 'value number'); - - label.dispose(); + assertVariable(session, scope, disposables, linkDetector, true); }); test('statusbar in debug mode', () => { diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index 6b47aa1dc3f..292fa730381 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { dispose } from 'vs/base/common/lifecycle'; import { URI as uri } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts index f5ec3d56c5a..e4473594e4c 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { ThemeIcon } from 'vs/base/common/themables'; import { Constants } from 'vs/base/common/uint'; @@ -41,7 +41,7 @@ export function createTestSession(model: DebugModel, name = 'mockSession', optio } }; } - } as IDebugService, undefined!, undefined!, new TestConfigurationService({ debug: { console: { collapseIdenticalLines: true } } }), undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService()); + } as IDebugService, undefined!, undefined!, new TestConfigurationService({ debug: { console: { collapseIdenticalLines: true } } }), undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService(), undefined!, undefined!); } function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame; secondStackFrame: StackFrame } { @@ -445,7 +445,7 @@ suite('Debug - CallStack', () => { override get state(): State { return State.Stopped; } - }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService()); + }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService(), undefined!, undefined!); disposables.add(session); const runningSession = createTestSession(model); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts index c92a240a220..41c662c091b 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isHTMLSpanElement } from 'vs/base/browser/dom'; import { Color, RGBA } from 'vs/base/common/color'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts index 71b53f1ab25..6cde637373c 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts index c45e6dba417..480c811fc87 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { findExpressionInStackFrame } from 'vs/workbench/contrib/debug/browser/debugHover'; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts index 7647820f3d0..61e92664e50 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; import { Emitter } from 'vs/base/common/event'; import { mockObject, MockObject } from 'vs/base/test/common/mock'; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugSession.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugSession.test.ts index a4b59eee38d..b58f2509a7b 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugSession.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugSession.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ThreadStatusScheduler } from 'vs/workbench/contrib/debug/browser/debugSession'; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugSource.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugSource.test.ts index 672ae3c3061..aad31249a2c 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugSource.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugSource.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; import { URI as uri } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugUtils.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugUtils.test.ts index 585497db087..c57bb374b3d 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugUtils.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugUtils.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfig } from 'vs/workbench/contrib/debug/common/debug'; import { formatPII, getExactExpressionStartAndEnd, getVisibleAndSorted } from 'vs/workbench/contrib/debug/common/debugUtils'; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugViewModel.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugViewModel.test.ts index d42de786cb5..6e2f76448ec 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugViewModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugViewModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts index 6a38dac9fdc..1b888a6e147 100644 --- a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isHTMLAnchorElement } from 'vs/base/browser/dom'; import { isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts b/src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts index 3f3549c604e..5a9f3f2f415 100644 --- a/src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mock, mockObject } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; diff --git a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts index f385cbbb7b3..54da8d61f19 100644 --- a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { timeout } from 'vs/base/common/async'; import severity from 'vs/base/common/severity'; diff --git a/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts b/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts new file mode 100644 index 00000000000..5eb511bc678 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as dom from 'vs/base/browser/dom'; +import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { Scope, StackFrame, Thread, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; +import { MockDebugService, MockSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { NullHoverService } from 'vs/platform/hover/test/browser/nullHoverService'; +import { IDebugService, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; +import { VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; +import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +const $ = dom.$; + +function assertVariable(disposables: Pick, variablesRenderer: VariablesRenderer, displayType: boolean) { + const session = new MockSession(); + const thread = new Thread(session, 'mockthread', 1); + const range = { + startLineNumber: 1, + startColumn: 1, + endLineNumber: undefined!, + endColumn: undefined! + }; + const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', range, 0, true); + const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); + const node = { + element: new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined, 0, 0, undefined, {}, 'string'), + depth: 0, + visibleChildrenCount: 1, + visibleChildIndex: -1, + collapsible: false, + collapsed: false, + visible: true, + filterData: undefined, + children: [] + }; + const expression = $('.'); + const name = $('.'); + const type = $('.'); + const value = $('.'); + const label = disposables.add(new HighlightedLabel(name)); + const lazyButton = $('.'); + const inputBoxContainer = $('.'); + const elementDisposable = disposables.add(new DisposableStore()); + const templateDisposable = disposables.add(new DisposableStore()); + const currentElement = undefined; + const data = { + expression, + name, + type, + value, + label, + lazyButton, + inputBoxContainer, + elementDisposable, + templateDisposable, + currentElement + }; + variablesRenderer.renderElement(node, 0, data); + assert.strictEqual(value.textContent, ''); + assert.strictEqual(label.element.textContent, 'foo'); + + node.element.value = 'xpto'; + variablesRenderer.renderElement(node, 0, data); + assert.strictEqual(value.textContent, 'xpto'); + assert.strictEqual(type.textContent, displayType ? 'string =' : ''); + assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); +} + +suite('Debug - Variable Debug View', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + let variablesRenderer: VariablesRenderer; + let instantiationService: TestInstantiationService; + let linkDetector: LinkDetector; + let configurationService: TestConfigurationService; + + setup(() => { + instantiationService = workbenchInstantiationService(undefined, disposables); + linkDetector = instantiationService.createInstance(LinkDetector); + const debugService = new MockDebugService(); + instantiationService.stub(IHoverService, NullHoverService); + debugService.getViewModel = () => { focusedStackFrame: undefined, getSelectedExpression: () => undefined }; + debugService.getViewModel().getSelectedExpression = () => undefined; + instantiationService.stub(IDebugService, debugService); + }); + + test('variable expressions with display type', () => { + configurationService = new TestConfigurationService({ + debug: { + showVariableTypes: true + } + }); + instantiationService.stub(IConfigurationService, configurationService); + variablesRenderer = instantiationService.createInstance(VariablesRenderer, linkDetector); + assertVariable(disposables, variablesRenderer, true); + }); + + test('variable expressions', () => { + configurationService = new TestConfigurationService({ + debug: { + showVariableTypes: false + } + }); + instantiationService.stub(IConfigurationService, configurationService); + variablesRenderer = instantiationService.createInstance(VariablesRenderer, linkDetector); + assertVariable(disposables, variablesRenderer, false); + }); +}); diff --git a/src/vs/workbench/contrib/debug/test/browser/watch.test.ts b/src/vs/workbench/contrib/debug/test/browser/watch.test.ts index dabf666859c..92b54afe7ca 100644 --- a/src/vs/workbench/contrib/debug/test/browser/watch.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/watch.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DebugModel, Expression } from 'vs/workbench/contrib/debug/common/debugModel'; import { createMockDebugModel } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; diff --git a/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts b/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts new file mode 100644 index 00000000000..220e79d6a84 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as dom from 'vs/base/browser/dom'; +import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { WatchExpressionsRenderer } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; +import { Scope, StackFrame, Thread, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; +import { MockDebugService, MockSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { NullHoverService } from 'vs/platform/hover/test/browser/nullHoverService'; +import { IDebugService, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +const $ = dom.$; + +function assertWatchVariable(disposables: Pick, watchExpressionsRenderer: WatchExpressionsRenderer, displayType: boolean) { + const session = new MockSession(); + const thread = new Thread(session, 'mockthread', 1); + const range = { + startLineNumber: 1, + startColumn: 1, + endLineNumber: undefined!, + endColumn: undefined! + }; + const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', range, 0, true); + const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); + const node = { + element: new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined, 0, 0, undefined, {}, 'string'), + depth: 0, + visibleChildrenCount: 1, + visibleChildIndex: -1, + collapsible: false, + collapsed: false, + visible: true, + filterData: undefined, + children: [] + }; + const expression = $('.'); + const name = $('.'); + const type = $('.'); + const value = $('.'); + const label = disposables.add(new HighlightedLabel(name)); + const lazyButton = $('.'); + const inputBoxContainer = $('.'); + const elementDisposable = disposables.add(new DisposableStore()); + const templateDisposable = disposables.add(new DisposableStore()); + const currentElement = undefined; + const data = { + expression, + name, + type, + value, + label, + lazyButton, + inputBoxContainer, + elementDisposable, + templateDisposable, + currentElement + }; + watchExpressionsRenderer.renderElement(node, 0, data); + assert.strictEqual(value.textContent, ''); + assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); + + node.element.value = 'xpto'; + watchExpressionsRenderer.renderElement(node, 0, data); + assert.strictEqual(value.textContent, 'xpto'); + assert.strictEqual(type.textContent, displayType ? 'string =' : ''); + assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); +} + +suite('Debug - Watch Debug View', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + let watchExpressionsRenderer: WatchExpressionsRenderer; + let instantiationService: TestInstantiationService; + let configurationService: TestConfigurationService; + + setup(() => { + instantiationService = workbenchInstantiationService(undefined, disposables); + const debugService = new MockDebugService(); + instantiationService.stub(IHoverService, NullHoverService); + debugService.getViewModel = () => { focusedStackFrame: undefined, getSelectedExpression: () => undefined }; + debugService.getViewModel().getSelectedExpression = () => undefined; + instantiationService.stub(IDebugService, debugService); + }); + + test('watch expressions with display type', () => { + configurationService = new TestConfigurationService({ + debug: { + showVariableTypes: true + } + }); + instantiationService.stub(IConfigurationService, configurationService); + watchExpressionsRenderer = instantiationService.createInstance(WatchExpressionsRenderer); + assertWatchVariable(disposables, watchExpressionsRenderer, true); + }); + + test('watch expressions', () => { + configurationService = new TestConfigurationService({ + debug: { + showVariableTypes: false + } + }); + instantiationService.stub(IConfigurationService, configurationService); + watchExpressionsRenderer = instantiationService.createInstance(WatchExpressionsRenderer); + assertWatchVariable(disposables, watchExpressionsRenderer, false); + }); +}); diff --git a/src/vs/workbench/contrib/debug/test/common/abstractDebugAdapter.test.ts b/src/vs/workbench/contrib/debug/test/common/abstractDebugAdapter.test.ts index 2246b7fa248..cfcc1af8b87 100644 --- a/src/vs/workbench/contrib/debug/test/common/abstractDebugAdapter.test.ts +++ b/src/vs/workbench/contrib/debug/test/common/abstractDebugAdapter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MockDebugAdapter } from 'vs/workbench/contrib/debug/test/common/mockDebug'; diff --git a/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts index 26c5549841b..e3cf88f9b07 100644 --- a/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DeferredPromise } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mockObject } from 'vs/base/test/common/mock'; diff --git a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts index 4d2d38f1f65..e618a4552da 100644 --- a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { join, normalize } from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; import { IDebugAdapterExecutable, IConfig, IDebugSession, IAdapterManager } from 'vs/workbench/contrib/debug/common/debug'; @@ -64,7 +64,8 @@ suite('Debug - Debugger', () => { 'debuggers': [ debuggerContribution ] - } + }, + enabledApiProposals: undefined, }; const extensionDescriptor1 = { @@ -89,7 +90,8 @@ suite('Debug - Debugger', () => { args: ['parg'] } ] - } + }, + enabledApiProposals: undefined, }; const extensionDescriptor2 = { @@ -122,7 +124,8 @@ suite('Debug - Debugger', () => { } } ] - } + }, + enabledApiProposals: undefined, }; diff --git a/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts index 9247a0ee636..89bb6968f5e 100644 --- a/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as crypto from 'crypto'; import * as net from 'net'; import * as platform from 'vs/base/common/platform'; diff --git a/src/vs/workbench/contrib/debug/test/node/terminals.test.ts b/src/vs/workbench/contrib/debug/test/node/terminals.test.ts index efdf2f940c3..bd81c9c13e2 100644 --- a/src/vs/workbench/contrib/debug/test/node/terminals.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/terminals.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { prepareCommand } from 'vs/workbench/contrib/debug/node/terminals'; diff --git a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts index c6be96b073d..244f3dfe5bc 100644 --- a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts +++ b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts @@ -20,7 +20,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { mock } from 'vs/base/test/common/mock'; import * as sinon from 'sinon'; -import * as assert from 'assert'; +import assert from 'assert'; import { ChangeType, FileType, IEditSessionsLogService, IEditSessionsStorageService } from 'vs/workbench/contrib/editSessions/common/editSessions'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; diff --git a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts index f3a0b28bf04..76d14f29f54 100644 --- a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts +++ b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts @@ -5,7 +5,7 @@ import { IGrammarContributions, EmmetEditorAction } from 'vs/workbench/contrib/emmet/browser/emmetActions'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/encryption/electron-sandbox/encryption.contribution.ts b/src/vs/workbench/contrib/encryption/electron-sandbox/encryption.contribution.ts index 9969928a2b5..48580f61c82 100644 --- a/src/vs/workbench/contrib/encryption/electron-sandbox/encryption.contribution.ts +++ b/src/vs/workbench/contrib/encryption/electron-sandbox/encryption.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isLinux } from 'vs/base/common/platform'; -import { stripComments } from 'vs/base/common/stripComments'; +import { parse } from 'vs/base/common/jsonc'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -34,7 +34,7 @@ class EncryptionContribution implements IWorkbenchContribution { } try { const content = await this.fileService.readFile(this.environmentService.argvResource); - const argv = JSON.parse(stripComments(content.value.toString())); + const argv = parse(content.value.toString()); if (argv['password-store'] === 'gnome' || argv['password-store'] === 'gnome-keyring') { this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['password-store'], value: 'gnome-libsecret' }], true); } diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 1ee623c22a0..a298cf3c232 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -369,14 +369,14 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { } else { title = nls.localize('extensionActivating', "Extension is activating..."); } - data.elementDisposables.push(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), data.activationTime, title)); + data.elementDisposables.push(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.activationTime, title)); clearNode(data.msgContainer); if (this._getUnresponsiveProfile(element.description.identifier)) { const el = $('span', undefined, ...renderLabelWithIcons(` $(alert) Unresponsive`)); const extensionHostFreezTitle = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze."); - data.elementDisposables.push(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), el, extensionHostFreezTitle)); + data.elementDisposables.push(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), el, extensionHostFreezTitle)); data.msgContainer.appendChild(el); } @@ -425,7 +425,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { 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.current) { const title = nls.localize('requests count title', "Last request was {0}.", fromNow(accessData.current.lastAccessed, true, true)); - data.elementDisposables.push(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), element, title)); + data.elementDisposables.push(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, title)); } data.msgContainer.appendChild(element); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index dc3c89dcdc8..5eacf022177 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -49,11 +49,11 @@ import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { ExtensionFeaturesTab } from 'vs/workbench/contrib/extensions/browser/extensionFeaturesTab'; import { - ActionWithDropDownAction, + ButtonWithDropDownExtensionAction, ClearLanguageAction, DisableDropDownAction, EnableDropDownAction, - ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, + ButtonWithDropdownExtensionActionViewItem, DropDownExtensionAction, ExtensionEditorManageExtensionAction, ExtensionStatusAction, ExtensionStatusLabelAction, @@ -71,7 +71,8 @@ import { UninstallAction, UpdateAction, WebInstallAction, - TogglePreReleaseExtensionAction + TogglePreReleaseExtensionAction, + ExtensionAction, } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { Delegate } from 'vs/workbench/contrib/extensions/browser/extensionsList'; import { ExtensionData, ExtensionsGridView, ExtensionsTree, getExtensions } from 'vs/workbench/contrib/extensions/browser/extensionsViewer'; @@ -200,7 +201,7 @@ class VersionWidget extends ExtensionWithDifferentGalleryVersionWidget { ) { super(); this.element = append(container, $('code.version')); - this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.element, localize('extension version', "Extension Version"))); + this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, localize('extension version', "Extension Version"))); this.render(); } render(): void { @@ -287,11 +288,11 @@ export class ExtensionEditor extends EditorPane { const details = append(header, $('.details')); const title = append(details, $('.title')); const name = append(title, $('span.name.clickable', { role: 'heading', tabIndex: 0 })); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), name, localize('name', "Extension name"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), name, localize('name', "Extension name"))); const versionWidget = new VersionWidget(title, this.hoverService); const preview = append(title, $('span.preview')); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), preview, localize('preview', "Preview"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), preview, localize('preview', "Preview"))); preview.textContent = localize('preview', "Preview"); const builtin = append(title, $('span.builtin')); @@ -299,7 +300,7 @@ export class ExtensionEditor extends EditorPane { const subtitle = append(details, $('.subtitle')); const publisher = append(append(subtitle, $('.subtitle-entry')), $('.publisher.clickable', { tabIndex: 0 })); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), publisher, localize('publisher', "Publisher"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), publisher, localize('publisher', "Publisher"))); publisher.setAttribute('role', 'button'); const publisherDisplayName = append(publisher, $('.publisher-name')); const verifiedPublisherWidget = this.instantiationService.createInstance(VerifiedPublisherWidget, append(publisher, $('.verified-publisher')), false); @@ -308,11 +309,11 @@ export class ExtensionEditor extends EditorPane { resource.setAttribute('role', 'button'); const installCount = append(append(subtitle, $('.subtitle-entry')), $('span.install', { tabIndex: 0 })); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), installCount, localize('install count', "Install count"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), installCount, localize('install count', "Install count"))); const installCountWidget = this.instantiationService.createInstance(InstallCountWidget, installCount, false); const rating = append(append(subtitle, $('.subtitle-entry')), $('span.rating.clickable', { tabIndex: 0 })); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), rating, localize('rating', "Rating"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), rating, localize('rating', "Rating"))); rating.setAttribute('role', 'link'); // #132645 const ratingsWidget = this.instantiationService.createInstance(RatingsWidget, rating, false); @@ -333,7 +334,7 @@ export class ExtensionEditor extends EditorPane { const actions = [ this.instantiationService.createInstance(ExtensionRuntimeStateAction), this.instantiationService.createInstance(ExtensionStatusLabelAction), - this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.updateActions', '', + this.instantiationService.createInstance(ButtonWithDropDownExtensionAction, 'extensions.updateActions', ExtensionAction.PROMINENT_LABEL_ACTION_CLASS, [[this.instantiationService.createInstance(UpdateAction, true)], [this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction, true, [true, 'onlyEnabledExtensions'])]]), this.instantiationService.createInstance(SetColorThemeAction), this.instantiationService.createInstance(SetFileIconThemeAction), @@ -348,11 +349,11 @@ export class ExtensionEditor extends EditorPane { this.instantiationService.createInstance(WebInstallAction), installAction, this.instantiationService.createInstance(InstallingLabelAction), - this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.uninstall', UninstallAction.UninstallLabel, [ + this.instantiationService.createInstance(ButtonWithDropDownExtensionAction, 'extensions.uninstall', UninstallAction.UninstallClass, [ [ this.instantiationService.createInstance(MigrateDeprecatedExtensionAction, false), this.instantiationService.createInstance(UninstallAction), - this.instantiationService.createInstance(InstallAnotherVersionAction), + this.instantiationService.createInstance(InstallAnotherVersionAction, null, true), ] ]), this.instantiationService.createInstance(TogglePreReleaseExtensionAction), @@ -363,11 +364,20 @@ export class ExtensionEditor extends EditorPane { const actionsAndStatusContainer = append(details, $('.actions-status-container')); const extensionActionBar = this._register(new ActionBar(actionsAndStatusContainer, { actionViewItemProvider: (action: IAction, options) => { - if (action instanceof ExtensionDropDownAction) { + if (action instanceof DropDownExtensionAction) { return action.createActionViewItem(options); } - if (action instanceof ActionWithDropDownAction) { - return new ExtensionActionWithDropdownActionViewItem(action, { ...options, icon: true, label: true, menuActionsOrProvider: { getActions: () => action.menuActions }, menuActionClassNames: (action.class || '').split(' ') }, this.contextMenuService); + if (action instanceof ButtonWithDropDownExtensionAction) { + return new ButtonWithDropdownExtensionActionViewItem( + action, + { + ...options, + icon: true, + label: true, + menuActionsOrProvider: { getActions: () => action.menuActions }, + menuActionClassNames: action.menuActionClassNames + }, + this.contextMenuService); } if (action instanceof ToggleAutoUpdateForExtensionAction) { return new CheckboxActionViewItem(undefined, action, { ...options, icon: true, label: true, checkboxStyles: defaultCheckboxStyles }); @@ -552,14 +562,14 @@ export class ExtensionEditor extends EditorPane { const workspaceFolder = this.contextService.getWorkspaceFolder(location); if (workspaceFolder && extension.isWorkspaceScoped) { template.resource.parentElement?.classList.add('clickable'); - this.transientDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), template.resource, this.uriIdentityService.extUri.relativePath(workspaceFolder.uri, location))); + this.transientDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.resource, this.uriIdentityService.extUri.relativePath(workspaceFolder.uri, location))); template.resource.textContent = localize('workspace extension', "Workspace Extension"); this.transientDisposables.add(onClick(template.resource, () => { this.viewsService.openView(EXPLORER_VIEW_ID, true).then(() => this.explorerService.select(location, true)); })); } else { template.resource.parentElement?.classList.remove('clickable'); - this.transientDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), template.resource, location.path)); + this.transientDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.resource, location.path)); template.resource.textContent = localize('local extension', "Local Extension"); } } @@ -774,7 +784,7 @@ export class ExtensionEditor extends EditorPane { return ''; } - const content = await renderMarkdownDocument(contents, this.extensionService, this.languageService, extension.type !== ExtensionType.System, false, token); + const content = await renderMarkdownDocument(contents, this.extensionService, this.languageService, { shouldSanitize: extension.type !== ExtensionType.System, token }); if (token?.isCancellationRequested) { return ''; } @@ -968,7 +978,7 @@ export class ExtensionEditor extends EditorPane { for (const [label, uri] of resources) { const resource = append(resourcesElement, $('a.resource', { tabindex: '0' }, label)); this.transientDisposables.add(onClick(resource, () => this.openerService.open(uri))); - this.transientDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), resource, uri.toString())); + this.transientDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), resource, uri.toString())); } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index 4152bf2f351..1f510c15b17 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -14,6 +14,7 @@ import { isString } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionRecommendationNotificationService, IExtensionRecommendations, RecommendationsNotificationResult, RecommendationSource, RecommendationSourceToString } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -283,9 +284,18 @@ export class ExtensionRecommendationNotificationService extends Disposable imple const installExtensions = async (isMachineScoped: boolean) => { this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); onDidInstallRecommendedExtensions(extensions); + const galleryExtensions: IGalleryExtension[] = [], resourceExtensions: IExtension[] = []; + for (const extension of extensions) { + if (extension.gallery) { + galleryExtensions.push(extension.gallery); + } else if (extension.resourceExtension) { + resourceExtensions.push(extension); + } + } await Promises.settled([ Promises.settled(extensions.map(extension => this.extensionsWorkbenchService.open(extension, { pinned: true }))), - this.extensionManagementService.installGalleryExtensions(extensions.map(e => ({ extension: e.gallery!, options: { isMachineScoped } }))) + galleryExtensions.length ? this.extensionManagementService.installGalleryExtensions(galleryExtensions.map(e => ({ extension: e, options: { isMachineScoped } }))) : Promise.resolve(), + resourceExtensions.length ? Promise.allSettled(resourceExtensions.map(r => this.extensionsWorkbenchService.install(r))) : Promise.resolve() ]); }; choices.push({ diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 0ff4dca2f71..33f961fc455 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -13,8 +13,8 @@ import { EnablementState, IExtensionManagementServerService, IWorkbenchExtension import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -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, IExtension, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +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, IExtension, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer, BuiltInExtensionsContext, SearchMarketplaceExtensionsContext, RecommendedExtensionsContext, DefaultViewsContext, ExtensionsSortByContext, SearchHasTextContext } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet'; @@ -49,7 +49,6 @@ import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/brow import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService'; -import { IExtensionService, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ResourceContextKey, WorkbenchStateContext } from 'vs/workbench/common/contextkeys'; @@ -65,7 +64,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; -import { Promises } from 'vs/base/common/async'; import { EditorExtensions } from 'vs/workbench/common/editor'; import { WORKSPACE_TRUST_EXTENSION_SUPPORT } from 'vs/workbench/services/workspaces/common/workspaceTrust'; import { ExtensionsCompletionItemsProvider } from 'vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider'; @@ -854,29 +852,51 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi when: ContextKeyExpr.and(ResourceContextKey.Extension.isEqualTo('.vsix'), ContextKeyExpr.or(CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER)), }], run: async (accessor: ServicesAccessor, resources: URI[] | URI) => { - const extensionService = accessor.get(IExtensionService); const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); const hostService = accessor.get(IHostService); const notificationService = accessor.get(INotificationService); - const extensions = Array.isArray(resources) ? resources : [resources]; - await Promises.settled(extensions.map(async (vsix) => await extensionsWorkbenchService.install(vsix))) - .then(async (extensions) => { - for (const extension of extensions) { - const requireReload = !(extension.local && extensionService.canAddExtension(toExtensionDescription(extension.local))); - const message = requireReload ? localize('InstallVSIXAction.successReload', "Completed installing {0} extension from VSIX. Please reload Visual Studio Code to enable it.", extension.displayName || extension.name) - : localize('InstallVSIXAction.success', "Completed installing {0} extension from VSIX.", extension.displayName || extension.name); - const actions = requireReload ? [{ - label: localize('InstallVSIXAction.reloadNow', "Reload Now"), - run: () => hostService.reload() - }] : []; - notificationService.prompt( - Severity.Info, - message, - actions - ); - } - }); + const vsixs = Array.isArray(resources) ? resources : [resources]; + const result = await Promise.allSettled(vsixs.map(async (vsix) => await extensionsWorkbenchService.install(vsix))); + let error: Error | undefined, requireReload = false, requireRestart = false; + for (const r of result) { + if (r.status === 'rejected') { + error = new Error(r.reason); + break; + } + requireReload = requireReload || r.value.runtimeState?.action === ExtensionRuntimeActionType.ReloadWindow; + requireRestart = requireRestart || r.value.runtimeState?.action === ExtensionRuntimeActionType.RestartExtensions; + } + if (error) { + throw error; + } + if (requireReload) { + notificationService.prompt( + Severity.Info, + localize('InstallVSIXAction.successReload', "Completed installing extension from VSIX. Please reload Visual Studio Code to enable it."), + [{ + label: localize('InstallVSIXAction.reloadNow', "Reload Now"), + run: () => hostService.reload() + }] + ); + } + else if (requireRestart) { + notificationService.prompt( + Severity.Info, + localize('InstallVSIXAction.successRestart', "Completed installing extension from VSIX. Please restart extensions to enable it."), + [{ + label: localize('InstallVSIXAction.restartExtensions', "Restart Extensions"), + run: () => extensionsWorkbenchService.updateRunningExtensions() + }] + ); + } + else { + notificationService.prompt( + Severity.Info, + localize('InstallVSIXAction.successNoReload', "Completed installing extension."), + [] + ); + } } }); @@ -1467,6 +1487,72 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi } }); + this.registerExtensionAction({ + id: 'workbench.extensions.action.installAndDonotSync', + title: localize('install installAndDonotSync', "Install (Do not Sync)"), + menu: { + id: MenuId.ExtensionContext, + group: '0_install', + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.not('extensionDisallowInstall'), CONTEXT_SYNC_ENABLEMENT), + order: 1 + }, + run: async (accessor: ServicesAccessor, extensionId: string) => { + const instantiationService = accessor.get(IInstantiationService); + const extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] + || (await this.extensionsWorkbenchService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; + if (extension) { + const action = instantiationService.createInstance(InstallAction, { + isMachineScoped: true, + }); + action.extension = extension; + return action.run(); + } + } + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.installPrereleaseAndDonotSync', + title: localize('installPrereleaseAndDonotSync', "Install Pre-Release (Do not Sync)"), + menu: { + id: MenuId.ExtensionContext, + group: '0_install', + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.not('extensionDisallowInstall'), CONTEXT_SYNC_ENABLEMENT), + order: 2 + }, + run: async (accessor: ServicesAccessor, extensionId: string) => { + const instantiationService = accessor.get(IInstantiationService); + const extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] + || (await this.extensionsWorkbenchService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; + if (extension) { + const action = instantiationService.createInstance(InstallAction, { + isMachineScoped: true, + preRelease: true + }); + action.extension = extension; + return action.run(); + } + } + }); + + this.registerExtensionAction({ + id: InstallAnotherVersionAction.ID, + title: InstallAnotherVersionAction.LABEL, + menu: { + id: MenuId.ExtensionContext, + group: '0_install', + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.not('extensionDisallowInstall')), + order: 3 + }, + run: async (accessor: ServicesAccessor, extensionId: string) => { + const instantiationService = accessor.get(IInstantiationService); + const extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] + || (await this.extensionsWorkbenchService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; + if (extension) { + return instantiationService.createInstance(InstallAnotherVersionAction, extension, false).run(); + } + } + }); + this.registerExtensionAction({ id: 'workbench.extensions.action.copyExtension', title: localize2('workbench.extensions.action.copyExtension', 'Copy'), @@ -1550,7 +1636,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '2_configure', - when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.equals('isWorkspaceScopedExtension', false)), + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.equals('isWorkspaceScopedExtension', false)), order: 4 }, run: async (accessor: ServicesAccessor, id: string) => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index fc174208ee6..fccbb361feb 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/extensionActions'; import { localize, localize2 } from 'vs/nls'; -import { IAction, Action, Separator, SubmenuAction } from 'vs/base/common/actions'; +import { IAction, Action, Separator, SubmenuAction, IActionChangeEvent } from 'vs/base/common/actions'; import { Delayer, Promises, Throttler } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; import { Emitter, Event } from 'vs/base/common/event'; @@ -15,7 +15,7 @@ import { disposeIfDisposable } from 'vs/base/common/lifecycle'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, AutoUpdateConfigurationKey, AutoUpdateConfigurationValue, ExtensionEditorTab, ExtensionRuntimeActionType, IExtensionArg } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { areSameExtensions, getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension, getWorkspaceSupportTypeMessage, TargetPlatform, isApplicationScopedExtension } from 'vs/platform/extensions/common/extensions'; @@ -52,7 +52,6 @@ import { IActionViewItemOptions, ActionViewItem } from 'vs/base/browser/ui/actio import { EXTENSIONS_CONFIG, IExtensionsConfigContent } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import { getErrorMessage, isCancellationError } from 'vs/base/common/errors'; import { IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; -import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; import { ILogService } from 'vs/platform/log/common/log'; import { errorIcon, infoIcon, manageExtensionIcon, syncEnabledIcon, syncIgnoredIcon, trustIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; @@ -73,6 +72,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { Registry } from 'vs/platform/registry/common/platform'; import { IUpdateService } from 'vs/platform/update/common/update'; +import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; export class PromptExtensionInstallFailureAction extends Action { @@ -134,7 +134,7 @@ export class PromptExtensionInstallFailureAction extends Action { return; } - if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleTargetPlatform, ExtensionManagementErrorCode.Malicious, ExtensionManagementErrorCode.Deprecated].includes(this.error.name)) { + if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleApi, ExtensionManagementErrorCode.IncompatibleTargetPlatform, ExtensionManagementErrorCode.Malicious, ExtensionManagementErrorCode.Deprecated].includes(this.error.name)) { await this.dialogService.info(getErrorMessage(this.error)); return; } @@ -216,21 +216,52 @@ export class PromptExtensionInstallFailureAction extends Action { } +export interface IExtensionActionChangeEvent extends IActionChangeEvent { + readonly hidden?: boolean; + readonly menuActions?: IAction[]; +} + export abstract class ExtensionAction extends Action implements IExtensionContainer { + + protected override _onDidChange = this._register(new Emitter()); + override readonly onDidChange = this._onDidChange.event; + static readonly EXTENSION_ACTION_CLASS = 'extension-action'; static readonly TEXT_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} text`; static readonly LABEL_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} label`; + static readonly PROMINENT_LABEL_ACTION_CLASS = `${ExtensionAction.LABEL_ACTION_CLASS} prominent`; static readonly ICON_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} icon`; + private _extension: IExtension | null = null; get extension(): IExtension | null { return this._extension; } set extension(extension: IExtension | null) { this._extension = extension; this.update(); } + + private _hidden: boolean = false; + get hidden(): boolean { return this._hidden; } + set hidden(hidden: boolean) { + if (this._hidden !== hidden) { + this._hidden = hidden; + this._onDidChange.fire({ hidden }); + } + } + + protected override _setEnabled(value: boolean): void { + super._setEnabled(value); + if (this.hideOnDisabled) { + this.hidden = !value; + } + } + + protected hideOnDisabled: boolean = true; + abstract update(): void; } -export class ActionWithDropDownAction extends ExtensionAction { +export class ButtonWithDropDownExtensionAction extends ExtensionAction { - private action: IAction | undefined; + private primaryAction: IAction | undefined; + readonly menuActionClassNames: string[] = []; private _menuActions: IAction[] = []; get menuActions(): IAction[] { return [...this._menuActions]; } @@ -246,10 +277,14 @@ export class ActionWithDropDownAction extends ExtensionAction { protected readonly extensionActions: ExtensionAction[]; constructor( - id: string, label: string, + id: string, + clazz: string, private readonly actionsGroups: ExtensionAction[][], ) { - super(id, label); + clazz = `${clazz} action-dropdown`; + super(id, undefined, clazz); + this.menuActionClassNames = clazz.split(' '); + this.hideOnDisabled = false; this.extensionActions = actionsGroups.flat(); this.update(); this._register(Event.any(...this.extensionActions.map(a => a.onDidChange))(() => this.update(true))); @@ -261,36 +296,35 @@ export class ActionWithDropDownAction extends ExtensionAction { this.extensionActions.forEach(a => a.update()); } - const enabledActionsGroups = this.actionsGroups.map(actionsGroup => actionsGroup.filter(a => a.enabled)); + const actionsGroups = this.actionsGroups.map(actionsGroup => actionsGroup.filter(a => !a.hidden)); let actions: IAction[] = []; - for (const enabledActions of enabledActionsGroups) { - if (enabledActions.length) { - actions = [...actions, ...enabledActions, new Separator()]; + for (const visibleActions of actionsGroups) { + if (visibleActions.length) { + actions = [...actions, ...visibleActions, new Separator()]; } } actions = actions.length ? actions.slice(0, actions.length - 1) : actions; - this.action = actions[0]; + this.primaryAction = actions[0]; this._menuActions = actions.length > 1 ? actions : []; + this._onDidChange.fire({ menuActions: this._menuActions }); - this.enabled = !!this.action; - if (this.action) { - this.label = this.getLabel(this.action as ExtensionAction); - this.tooltip = this.action.tooltip; - } - - let clazz = (this.action || this.extensionActions[0])?.class || ''; - clazz = clazz ? `${clazz} action-dropdown` : 'action-dropdown'; - if (this._menuActions.length === 0) { - clazz += ' action-dropdown'; + if (this.primaryAction) { + this.hidden = false; + this.enabled = this.primaryAction.enabled; + this.label = this.getLabel(this.primaryAction as ExtensionAction); + this.tooltip = this.primaryAction.tooltip; + } else { + this.hidden = true; + this.enabled = false; } - this.class = clazz; } - override run(): Promise { - const enabledActions = this.extensionActions.filter(a => a.enabled); - return enabledActions[0].run(); + override async run(): Promise { + if (this.enabled) { + await this.primaryAction?.run(); + } } protected getLabel(action: ExtensionAction): string { @@ -298,9 +332,42 @@ export class ActionWithDropDownAction extends ExtensionAction { } } +export class ButtonWithDropdownExtensionActionViewItem extends ActionWithDropdownActionViewItem { + + constructor( + action: ButtonWithDropDownExtensionAction, + options: IActionViewItemOptions & IActionWithDropdownActionViewItemOptions, + contextMenuProvider: IContextMenuProvider + ) { + super(null, action, options, contextMenuProvider); + this._register(action.onDidChange(e => { + if (e.hidden !== undefined || e.menuActions !== undefined) { + this.updateClass(); + } + })); + } + + override render(container: HTMLElement): void { + super.render(container); + this.updateClass(); + } + + protected override updateClass(): void { + super.updateClass(); + if (this.element && this.dropdownMenuActionViewItem?.element) { + this.element.classList.toggle('hide', (this._action).hidden); + const isMenuEmpty = (this._action).menuActions.length === 0; + this.element.classList.toggle('empty', isMenuEmpty); + this.dropdownMenuActionViewItem.element.classList.toggle('hide', isMenuEmpty); + } + } + +} + export class InstallAction extends ExtensionAction { - static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install`; + static readonly CLASS = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install`; + private static readonly HIDE = `${InstallAction.CLASS} hide`; protected _manifest: IExtensionManifest | null = null; set manifest(manifest: IExtensionManifest | null) { @@ -323,8 +390,9 @@ export class InstallAction extends ExtensionAction { @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, ) { - super('extensions.install', localize('install', "Install"), InstallAction.Class, false); - this.options = { ...options, isMachineScoped: false }; + super('extensions.install', localize('install', "Install"), InstallAction.CLASS, false); + this.hideOnDisabled = false; + this.options = { isMachineScoped: false, ...options }; this.update(); this._register(this.labelService.onDidChangeFormatters(() => this.updateLabel(), this)); } @@ -335,6 +403,8 @@ export class InstallAction extends ExtensionAction { protected async computeAndUpdateEnablement(): Promise { this.enabled = false; + this.class = InstallAction.HIDE; + this.hidden = true; if (!this.extension) { return; } @@ -344,8 +414,19 @@ export class InstallAction extends ExtensionAction { if (this.extensionsWorkbenchService.canSetLanguage(this.extension)) { return; } - if (this.extension.state === ExtensionState.Uninstalled && await this.extensionsWorkbenchService.canInstall(this.extension)) { - this.enabled = this.options.installPreReleaseVersion ? this.extension.hasPreReleaseVersion : this.extension.hasReleaseVersion; + if (this.extension.state !== ExtensionState.Uninstalled) { + return; + } + if (this.options.installPreReleaseVersion && !this.extension.hasPreReleaseVersion) { + return; + } + if (!this.options.installPreReleaseVersion && !this.extension.hasReleaseVersion) { + return; + } + this.hidden = false; + this.class = InstallAction.CLASS; + if (await this.extensionsWorkbenchService.canInstall(this.extension)) { + this.enabled = true; this.updateLabel(); } } @@ -518,7 +599,7 @@ export class InstallAction extends ExtensionAction { } -export class InstallDropdownAction extends ActionWithDropDownAction { +export class InstallDropdownAction extends ButtonWithDropDownExtensionAction { set manifest(manifest: IExtensionManifest | null) { this.extensionActions.forEach(a => (a).manifest = manifest); @@ -529,7 +610,7 @@ export class InstallDropdownAction extends ActionWithDropDownAction { @IInstantiationService instantiationService: IInstantiationService, @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, ) { - super(`extensions.installActions`, '', [ + super(`extensions.installActions`, InstallAction.CLASS, [ [ instantiationService.createInstance(InstallAction, { installPreReleaseVersion: extensionsWorkbenchService.preferPreReleases }), instantiationService.createInstance(InstallAction, { installPreReleaseVersion: !extensionsWorkbenchService.preferPreReleases }), @@ -562,8 +643,8 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { protected static readonly INSTALL_LABEL = localize('install', "Install"); protected static readonly INSTALLING_LABEL = localize('installing', "Installing"); - private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install`; - private static readonly InstallingClass = `${ExtensionAction.LABEL_ACTION_CLASS} install installing`; + private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install-other-server`; + private static readonly InstallingClass = `${ExtensionAction.LABEL_ACTION_CLASS} install-other-server installing`; updateWhenCounterExtensionChanges: boolean = true; @@ -721,7 +802,7 @@ export class UninstallAction extends ExtensionAction { static readonly UninstallLabel = localize('uninstallAction', "Uninstall"); private static readonly UninstallingLabel = localize('Uninstalling', "Uninstalling"); - private static readonly UninstallClass = `${ExtensionAction.LABEL_ACTION_CLASS} uninstall`; + static readonly UninstallClass = `${ExtensionAction.LABEL_ACTION_CLASS} uninstall`; private static readonly UnInstallingClass = `${ExtensionAction.LABEL_ACTION_CLASS} uninstall uninstalling`; constructor( @@ -987,32 +1068,7 @@ export class MigrateDeprecatedExtensionAction extends ExtensionAction { } } -export class ExtensionActionWithDropdownActionViewItem extends ActionWithDropdownActionViewItem { - - constructor( - action: ActionWithDropDownAction, - options: IActionViewItemOptions & IActionWithDropdownActionViewItemOptions, - contextMenuProvider: IContextMenuProvider - ) { - super(null, action, options, contextMenuProvider); - } - - override render(container: HTMLElement): void { - super.render(container); - this.updateClass(); - } - - protected override updateClass(): void { - super.updateClass(); - if (this.element && this.dropdownMenuActionViewItem && this.dropdownMenuActionViewItem.element) { - this.element.classList.toggle('empty', (this._action).menuActions.length === 0); - this.dropdownMenuActionViewItem.element.classList.toggle('hide', (this._action).menuActions.length === 0); - } - } - -} - -export abstract class ExtensionDropDownAction extends ExtensionAction { +export abstract class DropDownExtensionAction extends ExtensionAction { constructor( id: string, @@ -1024,9 +1080,9 @@ export abstract class ExtensionDropDownAction extends ExtensionAction { super(id, label, cssClass, enabled); } - private _actionViewItem: DropDownMenuActionViewItem | null = null; - createActionViewItem(options: IActionViewItemOptions): DropDownMenuActionViewItem { - this._actionViewItem = this.instantiationService.createInstance(DropDownMenuActionViewItem, this, options); + private _actionViewItem: DropDownExtensionActionViewItem | null = null; + createActionViewItem(options: IActionViewItemOptions): DropDownExtensionActionViewItem { + this._actionViewItem = this.instantiationService.createInstance(DropDownExtensionActionViewItem, this, options); return this._actionViewItem; } @@ -1036,10 +1092,10 @@ export abstract class ExtensionDropDownAction extends ExtensionAction { } } -export class DropDownMenuActionViewItem extends ActionViewItem { +export class DropDownExtensionActionViewItem extends ActionViewItem { constructor( - action: ExtensionDropDownAction, + action: DropDownExtensionAction, options: IActionViewItemOptions, @IContextMenuService private readonly contextMenuService: IContextMenuService ) { @@ -1084,6 +1140,7 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['isDefaultApplicationScopedExtension', extension.local && isApplicationScopedExtension(extension.local.manifest)]); cksOverlay.push(['isApplicationScopedExtension', extension.local && extension.local.isApplicationScoped]); cksOverlay.push(['isWorkspaceScopedExtension', extension.isWorkspaceScoped]); + cksOverlay.push(['isGalleryExtension', !!extension.identifier.uuid]); if (extension.local) { cksOverlay.push(['extensionSource', extension.local.source]); } @@ -1093,14 +1150,27 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['isExtensionRecommended', !!extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]]); cksOverlay.push(['isExtensionWorkspaceRecommended', extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]?.reasonId === ExtensionRecommendationReason.Workspace]); cksOverlay.push(['isUserIgnoredRecommendation', extensionIgnoredRecommendationsService.globalIgnoredRecommendations.some(e => e === extension.identifier.id.toLowerCase())]); - if (extension.state === ExtensionState.Installed) { - cksOverlay.push(['extensionStatus', 'installed']); + switch (extension.state) { + case ExtensionState.Installing: + cksOverlay.push(['extensionStatus', 'installing']); + break; + case ExtensionState.Installed: + cksOverlay.push(['extensionStatus', 'installed']); + break; + case ExtensionState.Uninstalling: + cksOverlay.push(['extensionStatus', 'uninstalling']); + break; + case ExtensionState.Uninstalled: + cksOverlay.push(['extensionStatus', 'uninstalled']); + break; } cksOverlay.push(['installedExtensionIsPreReleaseVersion', !!extension.local?.isPreReleaseVersion]); cksOverlay.push(['installedExtensionIsOptedToPreRelease', !!extension.local?.preRelease]); cksOverlay.push(['galleryExtensionIsPreReleaseVersion', !!extension.gallery?.properties.isPreReleaseVersion]); cksOverlay.push(['galleryExtensionHasPreReleaseVersion', extension.gallery?.hasPreReleaseVersion]); + cksOverlay.push(['extensionHasPreReleaseVersion', extension.hasPreReleaseVersion]); cksOverlay.push(['extensionHasReleaseVersion', extension.hasReleaseVersion]); + cksOverlay.push(['extensionDisallowInstall', !!extension.deprecationInfo?.disallowInstall]); const [colorThemes, fileIconThemes, productIconThemes] = await Promise.all([workbenchThemeService.getColorThemes(), workbenchThemeService.getFileIconThemes(), workbenchThemeService.getProductIconThemes()]); cksOverlay.push(['extensionHasColorThemes', colorThemes.some(theme => isThemeFromExtension(theme, extension))]); @@ -1137,7 +1207,7 @@ export async function getContextMenuActions(extension: IExtension | undefined | return toActions(actionsGroups, instantiationService); } -export class ManageExtensionAction extends ExtensionDropDownAction { +export class ManageExtensionAction extends DropDownExtensionAction { static readonly ID = 'extensions.manage'; @@ -1190,7 +1260,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction { } groups.push([ ...(installActions.length ? installActions : []), - this.instantiationService.createInstance(InstallAnotherVersionAction), + this.instantiationService.createInstance(InstallAnotherVersionAction, this.extension, false), this.instantiationService.createInstance(UninstallAction), ]); @@ -1221,7 +1291,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction { } } -export class ExtensionEditorManageExtensionAction extends ExtensionDropDownAction { +export class ExtensionEditorManageExtensionAction extends DropDownExtensionAction { constructor( private readonly contextKeyService: IContextKeyService, @@ -1351,26 +1421,36 @@ export class InstallAnotherVersionAction extends ExtensionAction { static readonly LABEL = localize('install another version', "Install Another Version..."); constructor( + extension: IExtension | null, + private readonly whenInstalled: boolean, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IWorkbenchExtensionManagementService private readonly extensionManagementService: IWorkbenchExtensionManagementService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IQuickInputService private readonly quickInputService: IQuickInputService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IDialogService private readonly dialogService: IDialogService, ) { super(InstallAnotherVersionAction.ID, InstallAnotherVersionAction.LABEL, ExtensionAction.LABEL_ACTION_CLASS); + this.extension = extension; this.update(); } update(): void { - this.enabled = !!this.extension && !this.extension.isBuiltin && !!this.extension.gallery && !!this.extension.local && !!this.extension.server && this.extension.state === ExtensionState.Installed && !this.extension.deprecationInfo; + this.enabled = !!this.extension && !this.extension.isBuiltin && !!this.extension.identifier.uuid && !this.extension.deprecationInfo; + if (this.enabled && this.whenInstalled) { + this.enabled = !!this.extension?.local && !!this.extension.server && this.extension.state === ExtensionState.Installed; + } } override async run(): Promise { if (!this.enabled) { return; } - const targetPlatform = await this.extension!.server!.extensionManagementService.getTargetPlatform(); - const allVersions = await this.extensionGalleryService.getAllCompatibleVersions(this.extension!.gallery!, this.extension!.local!.preRelease, targetPlatform); + if (!this.extension) { + return; + } + const targetPlatform = this.extension.server ? await this.extension.server.extensionManagementService.getTargetPlatform() : await this.extensionManagementService.getTargetPlatform(); + const allVersions = await this.extensionGalleryService.getAllCompatibleVersions(this.extension.identifier, this.extension.local?.preRelease ?? this.extension.gallery?.properties.isPreReleaseVersion ?? false, targetPlatform); if (!allVersions.length) { await this.dialogService.info(localize('no versions', "This extension has no other versions.")); return; @@ -1380,7 +1460,7 @@ export class InstallAnotherVersionAction extends ExtensionAction { return { id: v.version, label: v.version, - description: `${fromNow(new Date(Date.parse(v.date)), true)}${v.isPreReleaseVersion ? ` (${localize('pre-release', "pre-release")})` : ''}${v.version === this.extension!.version ? ` (${localize('current', "current")})` : ''}`, + description: `${fromNow(new Date(Date.parse(v.date)), true)}${v.isPreReleaseVersion ? ` (${localize('pre-release', "pre-release")})` : ''}${v.version === this.extension?.local?.manifest.version ? ` (${localize('current', "current")})` : ''}`, latest: i === 0, ariaLabel: `${v.isPreReleaseVersion ? 'Pre-Release version' : 'Release version'} ${v.version}`, isPreReleaseVersion: v.isPreReleaseVersion @@ -1392,18 +1472,18 @@ export class InstallAnotherVersionAction extends ExtensionAction { matchOnDetail: true }); if (pick) { - if (this.extension!.version === pick.id) { + if (this.extension.local?.manifest.version === pick.id) { return; } try { if (pick.latest) { - const [extension] = pick.id !== this.extension?.version ? await this.extensionsWorkbenchService.getExtensions([{ id: this.extension!.identifier.id, preRelease: pick.isPreReleaseVersion }], CancellationToken.None) : [this.extension]; - await this.extensionsWorkbenchService.install(extension ?? this.extension!, { installPreReleaseVersion: pick.isPreReleaseVersion }); + const [extension] = pick.id !== this.extension.version ? await this.extensionsWorkbenchService.getExtensions([{ id: this.extension.identifier.id, preRelease: pick.isPreReleaseVersion }], CancellationToken.None) : [this.extension]; + await this.extensionsWorkbenchService.install(extension ?? this.extension, { installPreReleaseVersion: pick.isPreReleaseVersion }); } else { - await this.extensionsWorkbenchService.install(this.extension!, { installPreReleaseVersion: pick.isPreReleaseVersion, version: pick.id }); + await this.extensionsWorkbenchService.install(this.extension, { installPreReleaseVersion: pick.isPreReleaseVersion, version: pick.id }); } } catch (error) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension!, pick.latest ? this.extension!.latestVersion : pick.id, InstallOperation.Install, error).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension, pick.latest ? this.extension.latestVersion : pick.id, InstallOperation.Install, error).run(); } } return null; @@ -1540,12 +1620,12 @@ export class DisableGloballyAction extends ExtensionAction { } } -export class EnableDropDownAction extends ActionWithDropDownAction { +export class EnableDropDownAction extends ButtonWithDropDownExtensionAction { constructor( @IInstantiationService instantiationService: IInstantiationService ) { - super('extensions.enable', localize('enableAction', "Enable"), [ + super('extensions.enable', ExtensionAction.LABEL_ACTION_CLASS, [ [ instantiationService.createInstance(EnableGloballyAction), instantiationService.createInstance(EnableForWorkspaceAction) @@ -1554,12 +1634,12 @@ export class EnableDropDownAction extends ActionWithDropDownAction { } } -export class DisableDropDownAction extends ActionWithDropDownAction { +export class DisableDropDownAction extends ButtonWithDropDownExtensionAction { constructor( @IInstantiationService instantiationService: IInstantiationService ) { - super('extensions.disable', localize('disableAction', "Disable"), [[ + super('extensions.disable', ExtensionAction.LABEL_ACTION_CLASS, [[ instantiationService.createInstance(DisableGloballyAction), instantiationService.createInstance(DisableForWorkspaceAction) ]]); @@ -2284,7 +2364,7 @@ export class ExtensionStatusLabelAction extends Action implements IExtensionCont } -export class ToggleSyncExtensionAction extends ExtensionDropDownAction { +export class ToggleSyncExtensionAction extends DropDownExtensionAction { private static readonly IGNORED_SYNC_CLASS = `${ExtensionAction.ICON_ACTION_CLASS} extension-sync ${ThemeIcon.asClassName(syncIgnoredIcon)}`; private static readonly SYNC_CLASS = `${ToggleSyncExtensionAction.ICON_ACTION_CLASS} extension-sync ${ThemeIcon.asClassName(syncEnabledIcon)}`; @@ -2723,16 +2803,14 @@ export class InstallSpecificVersionOfExtensionAction extends Action { override async run(): Promise { const extensionPick = await this.quickInputService.pick(this.getExtensionEntries(), { placeHolder: localize('selectExtension', "Select Extension"), matchOnDetail: true }); if (extensionPick && extensionPick.extension) { - const action = this.instantiationService.createInstance(InstallAnotherVersionAction); - action.extension = extensionPick.extension; + const action = this.instantiationService.createInstance(InstallAnotherVersionAction, extensionPick.extension, true); await action.run(); await this.instantiationService.createInstance(SearchExtensionsAction, extensionPick.extension.identifier.id).run(); } } private isEnabled(extension: IExtension): boolean { - const action = this.instantiationService.createInstance(InstallAnotherVersionAction); - action.extension = extension; + const action = this.instantiationService.createInstance(InstallAnotherVersionAction, extension, true); return action.enabled && !!extension.local && this.extensionEnablementService.isEnabled(extension.local); } @@ -3015,12 +3093,7 @@ registerColor('extensionButton.hoverBackground', { hcLight: null }, localize('extensionButtonHoverBackground', "Button background hover color for extension actions.")); -registerColor('extensionButton.separator', { - dark: buttonSeparator, - light: buttonSeparator, - hcDark: buttonSeparator, - hcLight: buttonSeparator -}, localize('extensionButtonSeparator', "Button separator color for extension actions")); +registerColor('extensionButton.separator', buttonSeparator, localize('extensionButtonSeparator', "Button separator color for extension actions")); export const extensionButtonProminentBackground = registerColor('extensionButton.prominentBackground', { dark: buttonBackground, diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 683098f05b6..d74439e6c52 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -13,13 +13,12 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ManageExtensionAction, ExtensionRuntimeStateAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction, ToggleAutoUpdateForExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { ManageExtensionAction, ExtensionRuntimeStateAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ButtonWithDropDownExtensionAction, InstallDropdownAction, InstallingLabelAction, ButtonWithDropdownExtensionActionViewItem, DropDownExtensionAction, WebInstallAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction, ToggleAutoUpdateForExtensionAction, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionActivationStatusWidget, PreReleaseBookmarkWidget, extensionVerifiedPublisherIconColor, VerifiedPublisherWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; -import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; import { WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; @@ -69,8 +68,8 @@ export class Renderer implements IPagedRenderer { @IInstantiationService private readonly instantiationService: IInstantiationService, @INotificationService private readonly notificationService: INotificationService, @IExtensionService private readonly extensionService: IExtensionService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { } @@ -100,10 +99,19 @@ export class Renderer implements IPagedRenderer { const publisherDisplayName = append(publisher, $('.publisher-name.ellipsis')); const actionbar = new ActionBar(footer, { actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { - if (action instanceof ActionWithDropDownAction) { - return new ExtensionActionWithDropdownActionViewItem(action, { ...options, icon: true, label: true, menuActionsOrProvider: { getActions: () => action.menuActions }, menuActionClassNames: (action.class || '').split(' ') }, this.contextMenuService); + if (action instanceof ButtonWithDropDownExtensionAction) { + return new ButtonWithDropdownExtensionActionViewItem( + action, + { + ...options, + icon: true, + label: true, + menuActionsOrProvider: { getActions: () => action.menuActions }, + menuActionClassNames: action.menuActionClassNames + }, + this.contextMenuService); } - if (action instanceof ExtensionDropDownAction) { + if (action instanceof DropDownExtensionAction) { return action.createActionViewItem(options); } return undefined; @@ -118,7 +126,7 @@ export class Renderer implements IPagedRenderer { this.instantiationService.createInstance(ExtensionStatusLabelAction), this.instantiationService.createInstance(MigrateDeprecatedExtensionAction, true), this.instantiationService.createInstance(ExtensionRuntimeStateAction), - this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.updateActions', '', + this.instantiationService.createInstance(ButtonWithDropDownExtensionAction, 'extensions.updateActions', ExtensionAction.PROMINENT_LABEL_ACTION_CLASS, [[this.instantiationService.createInstance(UpdateAction, false)], [this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction, true, [true, 'onlyEnabledExtensions'])]]), this.instantiationService.createInstance(InstallDropdownAction), this.instantiationService.createInstance(InstallingLabelAction), @@ -185,23 +193,8 @@ export class Renderer implements IPagedRenderer { data.extensionDisposables = dispose(data.extensionDisposables); - const computeEnablement = async () => { - if (extension.state === ExtensionState.Uninstalled) { - if (!!extension.deprecationInfo) { - return true; - } - if (this.extensionsWorkbenchService.canSetLanguage(extension)) { - return false; - } - return !(await this.extensionsWorkbenchService.canInstall(extension)); - } else if (extension.local && !isLanguagePackExtension(extension.local.manifest)) { - const runningExtension = this.extensionService.extensions.filter(e => areSameExtensions({ id: e.identifier.value }, extension.identifier))[0]; - return !(runningExtension && extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); - } - return false; - }; - const updateEnablement = async () => { - const disabled = await computeEnablement(); + const updateEnablement = () => { + const disabled = extension.state === ExtensionState.Installed && extension.local && !this.extensionEnablementService.isEnabled(extension.local); const deprecated = !!extension.deprecationInfo; data.element.classList.toggle('deprecated', deprecated); data.root.classList.toggle('disabled', disabled); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 7ca65d956ce..9a81262ecf8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -16,7 +16,7 @@ import { append, $, Dimension, hide, show, DragAndDropObserver, trackFocus } fro import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, AutoCheckUpdatesConfigurationKey, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu } from '../common/extensions'; +import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, AutoCheckUpdatesConfigurationKey, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, AutoRestartConfigurationKey } from '../common/extensions'; import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -846,7 +846,8 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution constructor( @IActivityService private readonly activityService: IActivityService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this.onServiceChange(); @@ -856,7 +857,7 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution private onServiceChange(): void { this.badgeHandle.clear(); - const actionRequired = this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined); + const actionRequired = this.configurationService.getValue(AutoRestartConfigurationKey) === true ? [] : this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined); const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) ? 1 : 0), 0); const newBadgeNumber = outdated + actionRequired.length; if (newBadgeNumber > 0) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 3d725460a20..4b53d91cbc5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -42,7 +42,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension | null = null; @@ -126,7 +126,7 @@ export class InstallCountWidget extends ExtensionWidget { export class RatingsWidget extends ExtensionWidget { - private readonly containerHover: IUpdatableHover; + private readonly containerHover: IManagedHover; constructor( private container: HTMLElement, @@ -140,7 +140,7 @@ export class RatingsWidget extends ExtensionWidget { container.classList.add('small'); } - this.containerHover = this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), container, '')); + this.containerHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, '')); this.render(); } @@ -192,7 +192,7 @@ export class RatingsWidget extends ExtensionWidget { export class VerifiedPublisherWidget extends ExtensionWidget { private readonly disposables = this._register(new DisposableStore()); - private readonly containerHover: IUpdatableHover; + private readonly containerHover: IManagedHover; constructor( private container: HTMLElement, @@ -201,7 +201,7 @@ export class VerifiedPublisherWidget extends ExtensionWidget { @IOpenerService private readonly openerService: IOpenerService, ) { super(); - this.containerHover = this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), container, '')); + this.containerHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, '')); this.render(); } @@ -258,7 +258,7 @@ export class SponsorWidget extends ExtensionWidget { } const sponsor = append(this.container, $('span.sponsor.clickable', { tabIndex: 0 })); - this.disposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), sponsor, this.extension?.publisherSponsorLink.toString() ?? '')); + this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), sponsor, this.extension?.publisherSponsorLink.toString() ?? '')); sponsor.setAttribute('role', 'link'); // #132645 const sponsorIconElement = renderIcon(sponsorIcon); const label = $('span', undefined, localize('sponsor', "Sponsor")); @@ -294,9 +294,7 @@ export class RecommendationWidget extends ExtensionWidget { } private clear(): void { - if (this.element) { - this.parent.removeChild(this.element); - } + this.element?.remove(); this.element = undefined; this.disposables.clear(); } @@ -330,9 +328,7 @@ export class PreReleaseBookmarkWidget extends ExtensionWidget { } private clear(): void { - if (this.element) { - this.parent.removeChild(this.element); - } + this.element?.remove(); this.element = undefined; this.disposables.clear(); } @@ -367,9 +363,7 @@ export class RemoteBadgeWidget extends ExtensionWidget { } private clear(): void { - if (this.remoteBadge.value) { - this.element.removeChild(this.remoteBadge.value.element); - } + this.remoteBadge.value?.element.remove(); this.remoteBadge.clear(); } @@ -386,7 +380,7 @@ export class RemoteBadgeWidget extends ExtensionWidget { class RemoteBadge extends Disposable { readonly element: HTMLElement; - readonly elementHover: IUpdatableHover; + readonly elementHover: IManagedHover; constructor( private readonly tooltip: boolean, @@ -397,7 +391,7 @@ class RemoteBadge extends Disposable { ) { super(); this.element = $('div.extension-badge.extension-remote-badge'); - this.elementHover = this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.element, '')); + this.elementHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, '')); this.render(); } @@ -478,7 +472,7 @@ export class SyncIgnoredWidget extends ExtensionWidget { if (this.extension && this.extension.state === ExtensionState.Installed && this.userDataSyncEnablementService.isEnabled() && this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension)) { const element = append(this.container, $('span.extension-sync-ignored' + ThemeIcon.asCSSSelector(syncIgnoredIcon))); - this.disposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), element, localize('syncingore.label', "This extension is ignored during sync."))); + this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, localize('syncingore.label', "This extension is ignored during sync."))); element.classList.add(...ThemeIcon.asClassNameArray(syncIgnoredIcon)); } } @@ -551,7 +545,7 @@ export class ExtensionHoverWidget extends ExtensionWidget { render(): void { this.hover.value = undefined; if (this.extension) { - this.hover.value = this.hoverService.setupUpdatableHover({ + this.hover.value = this.hoverService.setupManagedHover({ delay: this.configurationService.getValue('workbench.hover.delay'), showHover: (options, focus) => { return this.hoverService.showHover({ @@ -830,7 +824,7 @@ export class ExtensionRecommendationWidget extends ExtensionWidget { } export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hcDark: '#FF8E00', hcLight: textLinkForeground }, localize('extensionIconStarForeground', "The icon color for extension ratings."), true); -export const extensionVerifiedPublisherIconColor = registerColor('extensionIcon.verifiedForeground', { dark: textLinkForeground, light: textLinkForeground, hcDark: textLinkForeground, hcLight: textLinkForeground }, localize('extensionIconVerifiedForeground', "The icon color for extension verified publisher."), true); +export const extensionVerifiedPublisherIconColor = registerColor('extensionIcon.verifiedForeground', textLinkForeground, localize('extensionIconVerifiedForeground', "The icon color for extension verified publisher."), true); export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hcDark: '#1d9271', hcLight: textLinkForeground }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), true); export const extensionSponsorIconColor = registerColor('extensionIcon.sponsorForeground', { light: '#B51E78', dark: '#D758B3', hcDark: null, hcLight: '#B51E78' }, localize('extensionIcon.sponsorForeground', "The icon color for extension sponsor."), true); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 3eb8c7ee130..826da4474fb 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { firstOrDefault, index } from 'vs/base/common/arrays'; import { CancelablePromise, Promises, ThrottledDelayer, createCancelablePromise } from 'vs/base/common/async'; import { CancellationError, isCancellationError } from 'vs/base/common/errors'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IPager, singlePagePager } from 'vs/base/common/paging'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { @@ -18,19 +18,19 @@ import { IExtensionsControlManifest, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo, EXTENSION_IDENTIFIER_REGEX, InstallOptions, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension, extensionsConfigurationNodeBase } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { URI } from 'vs/base/common/uri'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType, AutoRestartConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url'; import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ILogService } from 'vs/platform/log/common/log'; import { IProgressOptions, IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService, NotificationPriority, Severity } from 'vs/platform/notification/common/notification'; import * as resources from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -58,6 +58,8 @@ import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator' import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ShowCurrentReleaseNotesActionId } from 'vs/workbench/contrib/update/common/update'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; interface IExtensionStateProvider { (extension: Extension): T; @@ -339,8 +341,9 @@ export class Extension implements IExtension { return !!this.gallery?.properties.isPreReleaseVersion; } + private _extensionEnabledWithPreRelease: boolean | undefined; get hasPreReleaseVersion(): boolean { - return !!this.gallery?.hasPreReleaseVersion || !!this.local?.hasPreReleaseVersion; + return !!this.gallery?.hasPreReleaseVersion || !!this.local?.hasPreReleaseVersion || !!this._extensionEnabledWithPreRelease; } get hasReleaseVersion(): boolean { @@ -498,6 +501,12 @@ ${this.description} return []; } + setExtensionsControlManifest(extensionsControlManifest: IExtensionsControlManifest): void { + this.isMalicious = extensionsControlManifest.malicious.some(identifier => areSameExtensions(this.identifier, identifier)); + this.deprecationInfo = extensionsControlManifest.deprecated ? extensionsControlManifest.deprecated[this.identifier.id.toLowerCase()] : undefined; + this._extensionEnabledWithPreRelease = extensionsControlManifest?.extensionsEnabledWithPreRelease?.includes(this.identifier.id.toLowerCase()); + } + private getManifestFromLocalOrResource(): IExtensionManifest | null { if (this.local) { return this.local.manifest; @@ -513,11 +522,6 @@ const EXTENSIONS_AUTO_UPDATE_KEY = 'extensions.autoUpdate'; class Extensions extends Disposable { - static updateExtensionFromControlManifest(extension: Extension, extensionsControlManifest: IExtensionsControlManifest): void { - extension.isMalicious = extensionsControlManifest.malicious.some(identifier => areSameExtensions(extension.identifier, identifier)); - extension.deprecationInfo = extensionsControlManifest.deprecated ? extensionsControlManifest.deprecated[extension.identifier.id.toLowerCase()] : undefined; - } - private readonly _onChange = this._register(new Emitter<{ extension: Extension; operation?: InstallOperation } | undefined>()); get onChange() { return this._onChange.event; } @@ -536,6 +540,7 @@ class Extensions extends Disposable { @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IWorkbenchExtensionManagementService private readonly workbenchExtensionManagementService: IWorkbenchExtensionManagementService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { @@ -544,7 +549,7 @@ class Extensions extends Disposable { this._register(server.extensionManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e))); this._register(server.extensionManagementService.onUninstallExtension(e => this.onUninstallExtension(e.identifier))); this._register(server.extensionManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e))); - this._register(server.extensionManagementService.onDidUpdateExtensionMetadata(e => this.onDidUpdateExtensionMetadata(e))); + this._register(server.extensionManagementService.onDidUpdateExtensionMetadata(e => this.onDidUpdateExtensionMetadata(e.local))); this._register(server.extensionManagementService.onDidChangeProfile(() => this.reset())); this._register(extensionEnablementService.onEnablementChanged(e => this.onEnablementChanged(e))); this._register(Event.any(this.onChange, this.onReset)(() => this._local = undefined)); @@ -666,7 +671,7 @@ class Extensions extends Disposable { const galleryWithLocalVersion: IGalleryExtension | undefined = (await this.galleryService.getExtensions([{ ...localExtension.identifier, version: localExtension.manifest.version }], CancellationToken.None))[0]; isPreReleaseVersion = !!galleryWithLocalVersion?.properties?.isPreReleaseVersion; } - return this.server.extensionManagementService.updateMetadata(localExtension, { id: gallery.identifier.uuid, publisherDisplayName: gallery.publisherDisplayName, publisherId: gallery.publisherId, isPreReleaseVersion }); + return this.server.extensionManagementService.updateMetadata(localExtension, { id: gallery.identifier.uuid, publisherDisplayName: gallery.publisherDisplayName, publisherId: gallery.publisherId, isPreReleaseVersion }, this.userDataProfileService.currentProfile.extensionsResource); } canInstall(galleryExtension: IGalleryExtension): Promise { @@ -720,7 +725,7 @@ class Extensions extends Disposable { const extension = byId[local.identifier.id] || this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined, undefined); extension.local = local; extension.enablementState = this.extensionEnablementService.getEnablementState(local); - Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); + extension.setExtensionsControlManifest(extensionsControlManifest); return extension; }); } @@ -756,7 +761,7 @@ class Extensions extends Disposable { if (!extension.gallery) { extension.gallery = gallery; } - Extensions.updateExtensionFromControlManifest(extension, await this.server.extensionManagementService.getExtensionsControlManifest()); + extension.setExtensionsControlManifest(await this.server.extensionManagementService.getExtensionsControlManifest()); extension.enablementState = this.extensionEnablementService.getEnablementState(local); } } @@ -949,9 +954,27 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension urlService.registerHandler(this); + if (this.productService.quality !== 'stable') { + this.registerAutoRestartConfig(); + } + this.whenInitialized = this.initialize(); } + private registerAutoRestartConfig(): void { + Registry.as(ConfigurationExtensions.Configuration) + .registerConfiguration({ + ...extensionsConfigurationNodeBase, + properties: { + [AutoRestartConfigurationKey]: { + type: 'boolean', + description: nls.localize('autoRestart', "If activated, extensions will automatically restart following an update if the window is not in focus."), + default: false, + } + } + }); + } + private async initialize(): Promise { // initialize local extensions await Promise.all([this.queryLocal(), this.extensionService.whenInstalledExtensionsRegistered()]); @@ -1011,6 +1034,25 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.autoUpdateBuiltinExtensions(); } } + + this.registerAutoRestartListener(); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(AutoRestartConfigurationKey)) { + this.registerAutoRestartListener(); + } + })); + } + + private readonly autoRestartListenerDisposable = this._register(new MutableDisposable()); + private registerAutoRestartListener(): void { + this.autoRestartListenerDisposable.value = undefined; + if (this.configurationService.getValue(AutoRestartConfigurationKey) === true) { + this.autoRestartListenerDisposable.value = this.hostService.onDidChangeFocus(focus => { + if (!focus && this.configurationService.getValue(AutoRestartConfigurationKey) === true) { + this.updateRunningExtensions(true); + } + }); + } } private reportInstalledExtensionsTelemetry() { @@ -1217,7 +1259,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension let extension = this.getInstalledExtensionMatchingGallery(gallery); if (!extension) { extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery, undefined); - Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); + (extension).setExtensionsControlManifest(extensionsControlManifest); } return extension; } @@ -1262,7 +1304,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return undefined; } - async updateRunningExtensions(): Promise { + async updateRunningExtensions(auto: boolean = false): Promise { const toAdd: ILocalExtension[] = []; const toRemove: string[] = []; @@ -1303,8 +1345,26 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (toAdd.length || toRemove.length) { - if (await this.extensionService.stopExtensionHosts(nls.localize('restart', "Enable or Disable extensions"))) { + if (await this.extensionService.stopExtensionHosts(nls.localize('restart', "Enable or Disable extensions"), auto)) { await this.extensionService.startExtensionHosts({ toAdd, toRemove }); + if (auto) { + this.notificationService.notify({ + severity: Severity.Info, + message: nls.localize('extensionsAutoRestart', "Extensions were auto restarted to enable updates."), + priority: NotificationPriority.SILENT + }); + } + type ExtensionsAutoRestartClassification = { + owner: 'sandy081'; + comment: 'Report when extensions are auto restarted'; + count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extensions auto restarted' }; + auto: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the restart was triggered automatically' }; + }; + type ExtensionsAutoRestartEvent = { + count: number; + auto: boolean; + }; + this.telemetryService.publicLog2('extensions:autorestart', { count: toAdd.length + toRemove.length, auto }); } } } @@ -1617,6 +1677,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension operation: InstallOperation.Update, installPreReleaseVersion: extension.local?.isPreReleaseVersion, profileLocation: this.userDataProfileService.currentProfile.extensionsResource, + isApplicationScoped: extension.local?.isApplicationScoped, } }); } @@ -1951,7 +2012,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (!extension && gallery) { extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery, undefined); - Extensions.updateExtensionFromControlManifest(extension as Extension, await this.extensionManagementService.getExtensionsControlManifest()); + (extension).setExtensionsControlManifest(await this.extensionManagementService.getExtensionsControlManifest()); } if (extension?.isMalicious) { throw new Error(nls.localize('malicious', "This extension is reported to be problematic.")); diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css index 7bca4243703..ed8c3395ccc 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css @@ -74,15 +74,15 @@ border-bottom: 1px solid var(--vscode-contrastBorder); } -.monaco-action-bar .action-item .action-label.extension-action.extension-status-error { +.monaco-action-bar .action-item .action-label.extension-action.extension-status-error::before { color: var(--vscode-editorError-foreground); } -.monaco-action-bar .action-item .action-label.extension-action.extension-status-warning { +.monaco-action-bar .action-item .action-label.extension-action.extension-status-warning::before { color: var(--vscode-editorWarning-foreground); } -.monaco-action-bar .action-item .action-label.extension-action.extension-status-info { +.monaco-action-bar .action-item .action-label.extension-action.extension-status-info::before { color: var(--vscode-editorInfo-foreground); } @@ -97,7 +97,8 @@ .monaco-action-bar .action-item.disabled .action-label.extension-action.hide, .monaco-action-bar .action-item.disabled .action-label.extension-action.ignore, .monaco-action-bar .action-item.disabled .action-label.extension-action.undo-ignore, -.monaco-action-bar .action-item.disabled .action-label.extension-action.install:not(.installing), +.monaco-action-bar .action-item .action-label.extension-action.install.hide, +.monaco-action-bar .action-item.disabled .action-label.extension-action.install-other-server:not(.installing), .monaco-action-bar .action-item.disabled .action-label.extension-action.uninstall:not(.uninstalling), .monaco-action-bar .action-item.disabled .action-label.extension-action.hide-when-disabled, .monaco-action-bar .action-item.disabled .action-label.extension-action.update, @@ -105,7 +106,7 @@ .monaco-action-bar .action-item.disabled .action-label.extension-action.theme, .monaco-action-bar .action-item.disabled .action-label.extension-action.language, .monaco-action-bar .action-item.disabled .action-label.extension-action.extension-sync, -.monaco-action-bar .action-item.action-dropdown-item.disabled, +.monaco-action-bar .action-item.action-dropdown-item.hide, .monaco-action-bar .action-item.action-dropdown-item .action-label.extension-action.hide, .monaco-action-bar .action-item.disabled .action-label.extension-action.reload, .monaco-action-bar .action-item.disabled .action-label.disable-status.hide, @@ -115,6 +116,10 @@ display: none; } +.monaco-action-bar .action-item.disabled .action-label.extension-action.label { + opacity: 0.4 !important; +} + .monaco-action-bar .action-item.checkbox-action-item.disabled { display: none; } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index a59e7c53d77..1de72e81ee9 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -485,6 +485,7 @@ overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + text-decoration: var(--text-link-decoration); } .extension-editor > .body > .content > .details > .additional-details-container .resources-container > .resources > .resource:hover { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css index d77881dfa39..9179fa761d3 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css @@ -39,6 +39,7 @@ .extension-verified-publisher > .extension-verified-publisher-domain { padding-left: 2px; color: var(--vscode-extensionIcon-verifiedForeground); + text-decoration: var(--text-link-decoration); } .extension-bookmark { diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 179624e281b..bba7f851350 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -163,6 +163,7 @@ export const ConfigurationKey = 'extensions'; export const AutoUpdateConfigurationKey = 'extensions.autoUpdate'; export const AutoCheckUpdatesConfigurationKey = 'extensions.autoCheckUpdates'; export const CloseExtensionDetailsOnViewChangeKey = 'extensions.closeExtensionDetailsOnViewChange'; +export const AutoRestartConfigurationKey = 'extensions.autoRestart'; export type AutoUpdateConfigurationValue = boolean | 'onlyEnabledExtensions' | 'onlySelectedExtensions'; diff --git a/src/vs/workbench/contrib/extensions/test/common/extensionQuery.test.ts b/src/vs/workbench/contrib/extensions/test/common/extensionQuery.test.ts index a743e9e184e..588f6a3cec5 100644 --- a/src/vs/workbench/contrib/extensions/test/common/extensionQuery.test.ts +++ b/src/vs/workbench/contrib/extensions/test/common/extensionQuery.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; suite('Extension query', () => { diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts index 8c96cf7a48e..efdcd3abd98 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ExtensionState } from 'vs/workbench/contrib/extensions/common/extensions'; import { Extension } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { IGalleryExtension, IGalleryExtensionProperties, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index 4e62c45838c..be1ca52ad70 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as sinon from 'sinon'; -import * as assert from 'assert'; +import assert from 'assert'; import * as uuid from 'vs/base/common/uuid'; import { IExtensionGalleryService, IGalleryExtensionAssets, IGalleryExtension, IExtensionManagementService, IExtensionTipsService, getTargetPlatform, diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index 00c2b9a6eec..197f6059031 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { generateUuid } from 'vs/base/common/uuid'; import { IExtensionsWorkbenchService, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions'; import * as ExtensionsActions from 'vs/workbench/contrib/extensions/browser/extensionsActions'; @@ -60,6 +60,9 @@ import { IUpdateService, State } from 'vs/platform/update/common/update'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { Mutable } from 'vs/base/common/types'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -126,6 +129,7 @@ function setupTest(disposables: Pick) { } }); + instantiationService.stub(IUserDataProfileService, disposables.add(new UserDataProfileService(toUserDataProfile('test', 'test', URI.file('foo'), URI.file('cache'))))); instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); instantiationService.stub(ILabelService, { onDidChangeFormatters: disposables.add(new Emitter()).event }); @@ -173,7 +177,7 @@ suite('ExtensionsActions', () => { testObject.extension = paged.firstPage[0]; assert.ok(!testObject.enabled); assert.strictEqual('Install', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install hide', testObject.class); }); }); }); @@ -187,7 +191,7 @@ suite('ExtensionsActions', () => { return workbenchService.queryGallery(CancellationToken.None) .then((paged) => { testObject.extension = paged.firstPage[0]; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); assert.ok(!testObject.enabled); assert.strictEqual('Installing', testObject.label); @@ -202,7 +206,7 @@ suite('ExtensionsActions', () => { const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); const paged = await workbenchService.queryGallery(CancellationToken.None); - const promise = Event.toPromise(testObject.onDidChange); + const promise = Event.toPromise(Event.filter(testObject.onDidChange, e => e.enabled === true)); testObject.extension = paged.firstPage[0]; await promise; assert.ok(testObject.enabled); @@ -217,8 +221,8 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - uninstallEvent.fire({ identifier: local.identifier }); - didUninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -232,8 +236,8 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - uninstallEvent.fire({ identifier: local.identifier }); - didUninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -255,7 +259,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); assert.strictEqual('Uninstalling', testObject.label); assert.strictEqual('extension-action label uninstall uninstalling', testObject.class); @@ -303,7 +307,7 @@ suite('ExtensionsActions', () => { const gallery = aGalleryExtension('a'); const extension = extensions[0]; extension.gallery = gallery; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); testObject.extension = extension; assert.ok(!testObject.enabled); }); @@ -318,9 +322,9 @@ suite('ExtensionsActions', () => { const paged = await instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None); testObject.extension = paged.firstPage[0]; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); const promise = Event.toPromise(testObject.onDidChange); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); await promise; assert.ok(testObject.enabled); @@ -429,7 +433,7 @@ suite('ExtensionsActions', () => { c(); } })); - installEvent.fire({ identifier: local.identifier, source: gallery }); + installEvent.fire({ identifier: local.identifier, source: gallery, profileLocation: null! }); }); }); @@ -480,7 +484,7 @@ suite('ExtensionsActions', () => { .then(page => { testObject.extension = page.firstPage[0]; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); assert.ok(!testObject.enabled); assert.strictEqual('extension-action icon manage codicon codicon-extensions-manage hide', testObject.class); assert.strictEqual('Manage', testObject.tooltip); @@ -495,9 +499,9 @@ suite('ExtensionsActions', () => { const paged = await instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None); testObject.extension = paged.firstPage[0]; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); const promise = Event.toPromise(testObject.onDidChange); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); await promise; assert.ok(testObject.enabled); @@ -529,7 +533,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); assert.strictEqual('extension-action icon manage codicon codicon-extensions-manage', testObject.class); @@ -735,7 +739,7 @@ suite('ExtensionsActions', () => { testObject.extension = page.firstPage[0]; disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); assert.ok(!testObject.enabled); }); }); @@ -748,7 +752,7 @@ suite('ExtensionsActions', () => { .then(extensions => { const testObject: ExtensionsActions.EnableDropDownAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableDropDownAction)); testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); }); @@ -929,7 +933,7 @@ suite('ExtensionsActions', () => { const testObject: ExtensionsActions.DisableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableGloballyAction)); testObject.extension = page.firstPage[0]; disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); assert.ok(!testObject.enabled); }); }); @@ -948,7 +952,7 @@ suite('ExtensionsActions', () => { const testObject: ExtensionsActions.DisableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableGloballyAction)); testObject.extension = extensions[0]; disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); }); @@ -976,7 +980,7 @@ suite('ExtensionRuntimeStateAction', () => { instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); const paged = await workbenchService.queryGallery(CancellationToken.None); testObject.extension = paged.firstPage[0]; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -989,7 +993,7 @@ suite('ExtensionRuntimeStateAction', () => { const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -1010,9 +1014,9 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = paged.firstPage[0]; assert.ok(!testObject.enabled); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); const promise = Event.toPromise(testObject.onDidChange); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); await promise; assert.ok(testObject.enabled); assert.strictEqual(testObject.tooltip, `Please restart extensions to enable this extension.`); @@ -1035,8 +1039,8 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = paged.firstPage[0]; assert.ok(!testObject.enabled); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); assert.ok(!testObject.enabled); }); @@ -1056,10 +1060,10 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = paged.firstPage[0]; const identifier = gallery.identifier; - installEvent.fire({ identifier, source: gallery }); - didInstallEvent.fire([{ identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, { identifier }) }]); - uninstallEvent.fire({ identifier }); - didUninstallEvent.fire({ identifier }); + installEvent.fire({ identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, { identifier }), profileLocation: null! }]); + uninstallEvent.fire({ identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -1080,8 +1084,8 @@ suite('ExtensionRuntimeStateAction', () => { const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); - didUninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(testObject.enabled); assert.strictEqual(testObject.tooltip, `Please restart extensions to complete the uninstallation of this extension.`); }); @@ -1101,8 +1105,8 @@ suite('ExtensionRuntimeStateAction', () => { const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); - didUninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -1121,13 +1125,13 @@ suite('ExtensionRuntimeStateAction', () => { const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); - didUninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); const gallery = aGalleryExtension('a'); const identifier = gallery.identifier; - installEvent.fire({ identifier, source: gallery }); - didInstallEvent.fire([{ identifier, source: gallery, operation: InstallOperation.Install, local }]); + installEvent.fire({ identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier, source: gallery, operation: InstallOperation.Install, local, profileLocation: null! }]); assert.ok(!testObject.enabled); }); @@ -1156,8 +1160,8 @@ suite('ExtensionRuntimeStateAction', () => { } })); const gallery = aGalleryExtension('a', { uuid: local.identifier.id, version: '1.0.2' }); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); }); }); @@ -1179,8 +1183,8 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = extensions[0]; const gallery = aGalleryExtension('a', { identifier: local.identifier, version: '1.0.2' }); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Update, local: aLocalExtension('a', gallery, gallery) }]); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Update, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); assert.ok(!testObject.enabled); }); @@ -1290,8 +1294,8 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = extensions[0]; const gallery = aGalleryExtension('a', { identifier: local.identifier, version: '1.0.2' }); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); await workbenchService.setEnablement(extensions[0], EnablementState.EnabledGlobally); await testObject.update(); assert.ok(testObject.enabled); @@ -1315,8 +1319,8 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = paged.firstPage[0]; assert.ok(!testObject.enabled); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', { ...gallery, ...{ contributes: { localizations: [{ languageId: 'de', translations: [] }] } } }, gallery) }]); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', { ...gallery, ...{ contributes: { localizations: [{ languageId: 'de', translations: [] }] } } }, gallery), profileLocation: null! }]); assert.ok(!testObject.enabled); }); @@ -1337,8 +1341,8 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = extensions[0]; const gallery = aGalleryExtension('a', { uuid: local.identifier.id, version: '1.0.2' }); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', { ...gallery, ...{ contributes: { localizations: [{ languageId: 'de', translations: [] }] } } }, gallery) }]); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', { ...gallery, ...{ contributes: { localizations: [{ languageId: 'de', translations: [] }] } } }, gallery), profileLocation: null! }]); assert.ok(!testObject.enabled); }); @@ -1378,7 +1382,7 @@ suite('ExtensionRuntimeStateAction', () => { const remoteExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeRemote }) }); const localExtensionManagementService = createExtensionManagementService([localExtension]); const uninstallEvent = new Emitter(); - const onDidUninstallEvent = new Emitter<{ identifier: IExtensionIdentifier }>(); + const onDidUninstallEvent = new Emitter<{ identifier: IExtensionIdentifier; profileLocation: URI }>(); localExtensionManagementService.onUninstallExtension = uninstallEvent.event; localExtensionManagementService.onDidUninstallExtension = onDidUninstallEvent.event; const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, localExtensionManagementService, createExtensionManagementService([remoteExtension])); @@ -1404,8 +1408,8 @@ suite('ExtensionRuntimeStateAction', () => { assert.ok(testObject.extension); assert.ok(!testObject.enabled); - uninstallEvent.fire({ identifier: localExtension.identifier }); - didUninstallEvent.fire({ identifier: localExtension.identifier }); + uninstallEvent.fire({ identifier: localExtension.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: localExtension.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -1442,7 +1446,7 @@ suite('ExtensionRuntimeStateAction', () => { const remoteExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeRemote }) }); const promise = Event.toPromise(testObject.onDidChange); - onDidInstallEvent.fire([{ identifier: remoteExtension.identifier, local: remoteExtension, operation: InstallOperation.Install }]); + onDidInstallEvent.fire([{ identifier: remoteExtension.identifier, local: remoteExtension, operation: InstallOperation.Install, profileLocation: null! }]); await promise; assert.ok(testObject.enabled); @@ -1481,7 +1485,7 @@ suite('ExtensionRuntimeStateAction', () => { const localExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file('pub.a') }); const promise = Event.toPromise(Event.filter(testObject.onDidChange, () => testObject.enabled)); - onDidInstallEvent.fire([{ identifier: localExtension.identifier, local: localExtension, operation: InstallOperation.Install }]); + onDidInstallEvent.fire([{ identifier: localExtension.identifier, local: localExtension, operation: InstallOperation.Install, profileLocation: null! }]); await promise; assert.ok(testObject.enabled); @@ -1729,7 +1733,7 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test remote install action when installing local workspace extension', async () => { @@ -1755,12 +1759,12 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); - onInstallExtension.fire({ identifier: localWorkspaceExtension.identifier, source: gallery }); + onInstallExtension.fire({ identifier: localWorkspaceExtension.identifier, source: gallery, profileLocation: null! }); assert.ok(testObject.enabled); assert.strictEqual('Installing', testObject.label); - assert.strictEqual('extension-action label install installing', testObject.class); + assert.strictEqual('extension-action label install-other-server installing', testObject.class); }); test('Test remote install action when installing local workspace extension is finished', async () => { @@ -1788,16 +1792,16 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); - onInstallExtension.fire({ identifier: localWorkspaceExtension.identifier, source: gallery }); + onInstallExtension.fire({ identifier: localWorkspaceExtension.identifier, source: gallery, profileLocation: null! }); assert.ok(testObject.enabled); assert.strictEqual('Installing', testObject.label); - assert.strictEqual('extension-action label install installing', testObject.class); + assert.strictEqual('extension-action label install-other-server installing', testObject.class); const installedExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const promise = Event.toPromise(testObject.onDidChange); - onDidInstallEvent.fire([{ identifier: installedExtension.identifier, local: installedExtension, operation: InstallOperation.Install }]); + onDidInstallEvent.fire([{ identifier: installedExtension.identifier, local: installedExtension, operation: InstallOperation.Install, profileLocation: null! }]); await promise; assert.ok(!testObject.enabled); }); @@ -1822,7 +1826,7 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test remote install action is enabled local workspace+ui extension', async () => { @@ -1844,7 +1848,7 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test remote install action is enabled for local ui+workapace extension if can install is true', async () => { @@ -1866,7 +1870,7 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test remote install action is disabled for local ui+workapace extension if can install is false', async () => { @@ -1987,7 +1991,7 @@ suite('RemoteInstallAction', () => { assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - uninstallEvent.fire({ identifier: localWorkspaceExtension.identifier }); + uninstallEvent.fire({ identifier: localWorkspaceExtension.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -2107,7 +2111,7 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test remote install action is disabled if local language pack extension is uninstalled', async () => { @@ -2131,7 +2135,7 @@ suite('RemoteInstallAction', () => { assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - uninstallEvent.fire({ identifier: languagePackExtension.identifier }); + uninstallEvent.fire({ identifier: languagePackExtension.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); }); @@ -2160,7 +2164,7 @@ suite('LocalInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test local install action is enabled for remote ui+workspace extension', async () => { @@ -2181,7 +2185,7 @@ suite('LocalInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test local install action when installing remote ui extension', async () => { @@ -2207,12 +2211,12 @@ suite('LocalInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); - onInstallExtension.fire({ identifier: remoteUIExtension.identifier, source: gallery }); + onInstallExtension.fire({ identifier: remoteUIExtension.identifier, source: gallery, profileLocation: null! }); assert.ok(testObject.enabled); assert.strictEqual('Installing', testObject.label); - assert.strictEqual('extension-action label install installing', testObject.class); + assert.strictEqual('extension-action label install-other-server installing', testObject.class); }); test('Test local install action when installing remote ui extension is finished', async () => { @@ -2240,16 +2244,16 @@ suite('LocalInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); - onInstallExtension.fire({ identifier: remoteUIExtension.identifier, source: gallery }); + onInstallExtension.fire({ identifier: remoteUIExtension.identifier, source: gallery, profileLocation: null! }); assert.ok(testObject.enabled); assert.strictEqual('Installing', testObject.label); - assert.strictEqual('extension-action label install installing', testObject.class); + assert.strictEqual('extension-action label install-other-server installing', testObject.class); const installedExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`) }); const promise = Event.toPromise(testObject.onDidChange); - onDidInstallEvent.fire([{ identifier: installedExtension.identifier, local: installedExtension, operation: InstallOperation.Install }]); + onDidInstallEvent.fire([{ identifier: installedExtension.identifier, local: installedExtension, operation: InstallOperation.Install, profileLocation: null! }]); await promise; assert.ok(!testObject.enabled); }); @@ -2274,7 +2278,7 @@ suite('LocalInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test local install action is disabled when extension is not set', async () => { @@ -2399,7 +2403,7 @@ suite('LocalInstallAction', () => { assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - uninstallEvent.fire({ identifier: remoteUIExtension.identifier }); + uninstallEvent.fire({ identifier: remoteUIExtension.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -2498,7 +2502,7 @@ suite('LocalInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test local install action is disabled if remote language pack extension is uninstalled', async () => { @@ -2522,7 +2526,7 @@ suite('LocalInstallAction', () => { assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - uninstallEvent.fire({ identifier: languagePackExtension.identifier }); + uninstallEvent.fire({ identifier: languagePackExtension.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -2638,7 +2642,7 @@ function createExtensionManagementService(installed: ILocalExtension[] = []): IP getInstalled: () => Promise.resolve(installed), canInstall: async (extension: IGalleryExtension) => { return true; }, installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')), - updateMetadata: async (local: Mutable, metadata: Partial) => { + updateMetadata: async (local: Mutable, metadata: Partial, profileLocation: URI) => { local.identifier.uuid = metadata.id; local.publisherDisplayName = metadata.publisherDisplayName!; local.publisherId = metadata.publisherId!; @@ -2648,5 +2652,3 @@ function createExtensionManagementService(installed: ILocalExtension[] = []): IP async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, }; } - - diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index 7ff4ea1b27a..6d62ecebd61 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { generateUuid } from 'vs/base/common/uuid'; import { ExtensionsListView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; @@ -51,6 +51,9 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { IUpdateService, State } from 'vs/platform/update/common/update'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; suite('ExtensionsViews Tests', () => { @@ -122,6 +125,7 @@ suite('ExtensionsViews Tests', () => { }); instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); + instantiationService.stub(IUserDataProfileService, disposableStore.add(new UserDataProfileService(toUserDataProfile('test', 'test', URI.file('foo'), URI.file('cache'))))); const reasons: { [key: string]: any } = {}; reasons[workspaceRecommendationA.identifier.id] = { reasonId: ExtensionRecommendationReason.Workspace }; @@ -570,4 +574,3 @@ suite('ExtensionsViews Tests', () => { } }); - diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index e42cf86167d..95f83c91d4a 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as sinon from 'sinon'; -import * as assert from 'assert'; +import assert from 'assert'; import { generateUuid } from 'vs/base/common/uuid'; import { ExtensionState, AutoCheckUpdatesConfigurationKey, AutoUpdateConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; @@ -54,6 +54,9 @@ import { Mutable } from 'vs/base/common/types'; import { IUpdateService, State } from 'vs/platform/update/common/update'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -89,6 +92,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { stubConfiguration(); instantiationService.stub(IRemoteAgentService, RemoteAgentService); + instantiationService.stub(IUserDataProfileService, disposableStore.add(new UserDataProfileService(toUserDataProfile('test', 'test', URI.file('foo'), URI.file('cache'))))); instantiationService.stub(IWorkbenchExtensionManagementService, { onDidInstallExtensions: didInstallEvent.event, @@ -374,7 +378,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const identifier = gallery.identifier; // Installing - installEvent.fire({ identifier, source: gallery }); + installEvent.fire({ identifier, source: gallery, profileLocation: null! }); const local = testObject.local; assert.strictEqual(1, local.length); const actual = local[0]; @@ -382,18 +386,18 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(ExtensionState.Installing, actual.state); // Installed - didInstallEvent.fire([{ identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension(gallery.name, gallery, { identifier }) }]); + didInstallEvent.fire([{ identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension(gallery.name, gallery, { identifier }), profileLocation: null! }]); assert.strictEqual(ExtensionState.Installed, actual.state); assert.strictEqual(1, testObject.local.length); testObject.uninstall(actual); // Uninstalling - uninstallEvent.fire({ identifier }); + uninstallEvent.fire({ identifier, profileLocation: null! }); assert.strictEqual(ExtensionState.Uninstalling, actual.state); // Uninstalled - didUninstallEvent.fire({ identifier }); + didUninstallEvent.fire({ identifier, profileLocation: null! }); assert.strictEqual(ExtensionState.Uninstalled, actual.state); assert.strictEqual(0, testObject.local.length); @@ -416,8 +420,8 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject = await aWorkbenchService(); const target = testObject.local[0]; testObject.uninstall(target); - uninstallEvent.fire({ identifier: local.identifier }); - didUninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!(await testObject.canInstall(target))); }); @@ -455,11 +459,11 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extension = page.firstPage[0]; assert.strictEqual(ExtensionState.Uninstalled, extension.state); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); const promise = Event.toPromise(testObject.onChange); // Installed - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension(gallery.name, gallery, gallery) }]); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension(gallery.name, gallery, gallery), profileLocation: null! }]); await promise; }); @@ -477,7 +481,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { disposableStore.add(testObject.onChange(target)); // Installing - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); assert.ok(target.calledOnce); }); @@ -491,7 +495,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject.uninstall(testObject.local[0]); disposableStore.add(testObject.onChange(target)); - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(target.calledOnce); }); @@ -503,9 +507,9 @@ suite('ExtensionsWorkbenchServiceTest', () => { const target = sinon.spy(); testObject.uninstall(testObject.local[0]); - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); disposableStore.add(testObject.onChange(target)); - didUninstallEvent.fire({ identifier: local.identifier }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(target.calledOnce); }); @@ -1018,7 +1022,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject = await aWorkbenchService(); const local = aLocalExtension('pub.a'); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - didInstallEvent.fire([{ local, identifier: local.identifier, operation: InstallOperation.Update }]); + didInstallEvent.fire([{ local, identifier: local.identifier, operation: InstallOperation.Update, profileLocation: null! }]); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const actual = await testObject.queryLocal(); assert.strictEqual(actual[0].enablementState, EnablementState.DisabledGlobally); @@ -1028,7 +1032,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject = await aWorkbenchService(); const local = aLocalExtension('pub.a'); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledWorkspace); - didInstallEvent.fire([{ local, identifier: local.identifier, operation: InstallOperation.Update }]); + didInstallEvent.fire([{ local, identifier: local.identifier, operation: InstallOperation.Update, profileLocation: null! }]); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const actual = await testObject.queryLocal(); assert.strictEqual(actual[0].enablementState, EnablementState.DisabledWorkspace); @@ -1740,7 +1744,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { onDidUpdateExtensionMetadata: Event.None, getInstalled: () => Promise.resolve(installed), installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')), - updateMetadata: async (local: Mutable, metadata: Partial) => { + updateMetadata: async (local: Mutable, metadata: Partial, profileLocation: URI) => { local.identifier.uuid = metadata.id; local.publisherDisplayName = metadata.publisherDisplayName!; local.publisherId = metadata.publisherId!; diff --git a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts index 6ca5f7d48f0..3388ec7baea 100644 --- a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts @@ -138,11 +138,11 @@ export class ExternalTerminalContribution extends Disposable implements IWorkben MenuRegistry.appendMenuItem(MenuId.ExplorerContext, this._openInTerminalMenuItem); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, this._openInIntegratedTerminalMenuItem); - this._configurationService.onDidChangeConfiguration(e => { + this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('terminal.explorerKind') || e.affectsConfiguration('terminal.external')) { this._refreshOpenInTerminalMenuItemTitle(); } - }); + })); this._refreshOpenInTerminalMenuItemTitle(); } diff --git a/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts b/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts index 842599bb6be..5be58d57365 100644 --- a/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts +++ b/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 3e98384f357..58a3df13a9f 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -1110,18 +1110,32 @@ export const pasteFileHandler = async (accessor: ServicesAccessor, fileList?: Fi const configurationService = accessor.get(IConfigurationService); const uriIdentityService = accessor.get(IUriIdentityService); const dialogService = accessor.get(IDialogService); + const hostService = accessor.get(IHostService); const context = explorerService.getContext(false); const hasNativeFilesToPaste = fileList && fileList.length > 0; const confirmPasteNative = hasNativeFilesToPaste && configurationService.getValue('explorer.confirmPasteNative'); - const toPaste = await getFilesToPaste(fileList, clipboardService); + const toPaste = await getFilesToPaste(fileList, clipboardService, hostService); if (confirmPasteNative && toPaste.files.length >= 1) { const message = toPaste.files.length > 1 ? nls.localize('confirmMultiPasteNative', "Are you sure you want to paste the following {0} items?", toPaste.files.length) : nls.localize('confirmPasteNative', "Are you sure you want to paste '{0}'?", basename(toPaste.type === 'paths' ? toPaste.files[0].fsPath : toPaste.files[0].name)); - const detail = toPaste.files.length > 1 ? getFileNamesMessage(toPaste.files.map(item => toPaste.type === 'paths' ? item.path : (item as File).name)) : undefined; + const detail = toPaste.files.length > 1 ? getFileNamesMessage(toPaste.files.map(item => { + if (URI.isUri(item)) { + return item.fsPath; + } + + if (toPaste.type === 'paths') { + const path = hostService.getPathForFile(item); + if (path) { + return path; + } + } + + return item.name; + })) : undefined; const confirmation = await dialogService.confirm({ message, detail, @@ -1270,16 +1284,16 @@ type FilesToPaste = | { type: 'paths'; files: URI[] } | { type: 'data'; files: File[] }; -async function getFilesToPaste(fileList: FileList | undefined, clipboardService: IClipboardService): Promise { +async function getFilesToPaste(fileList: FileList | undefined, clipboardService: IClipboardService, hostService: IHostService): Promise { if (fileList && fileList.length > 0) { // with a `fileList` we support natively pasting file from disk from clipboard - const resources = [...fileList].filter(file => !!file.path && isAbsolute(file.path)).map(file => URI.file(file.path)); + const resources = [...fileList].map(file => hostService.getPathForFile(file)).filter(filePath => !!filePath && isAbsolute(filePath)).map((filePath) => URI.file(filePath!)); if (resources.length) { return { type: 'paths', files: resources, }; } // Support pasting files that we can't read from disk - return { type: 'data', files: [...fileList].filter(file => !file.path) }; + return { type: 'data', files: [...fileList].filter(file => !hostService.getPathForFile(file)) }; } else { // otherwise we fallback to reading resources from our clipboard service return { type: 'paths', files: resources.distinctParents(await clipboardService.readResources(), resource => resource) }; diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 9ff16b6046d..143f87a926a 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -23,7 +23,7 @@ import { IEditorPaneRegistry, EditorPaneDescriptor } from 'vs/workbench/browser/ import { ILabelService } from 'vs/platform/label/common/label'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExplorerService, UNDO_REDO_SOURCE } from 'vs/workbench/contrib/files/browser/explorerService'; -import { SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/encoding'; +import { GUESSABLE_ENCODINGS, SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/encoding'; import { Schemas } from 'vs/base/common/network'; import { WorkspaceWatcher } from 'vs/workbench/contrib/files/browser/workspaceWatcher'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; @@ -205,6 +205,17 @@ configurationRegistry.registerConfiguration({ 'markdownDescription': nls.localize('autoGuessEncoding', "When enabled, the editor will attempt to guess the character set encoding when opening files. This setting can also be configured per language. Note, this setting is not respected by text search. Only {0} is respected.", '`#files.encoding#`'), 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE }, + 'files.candidateGuessEncodings': { + 'type': 'array', + 'items': { + 'type': 'string', + 'enum': Object.keys(GUESSABLE_ENCODINGS), + 'enumDescriptions': Object.keys(GUESSABLE_ENCODINGS).map(key => GUESSABLE_ENCODINGS[key].labelLong) + }, + 'default': [], + 'markdownDescription': nls.localize('candidateGuessEncodings', "List of character set encodings that the editor should attempt to guess in the order they are listed. In case it cannot be determined, {0} is respected", '`#files.encoding#`'), + 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE + }, 'files.eol': { 'type': 'string', 'enum': [ @@ -273,13 +284,13 @@ configurationRegistry.registerConfiguration({ 'files.autoSaveWorkspaceFilesOnly': { 'type': 'boolean', 'default': false, - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveWorkspaceFilesOnly' }, "When enabled, will limit [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors to files that are inside the opened workspace. Only applies when `#files.autoSave#` is enabled."), + 'markdownDescription': nls.localize('autoSaveWorkspaceFilesOnly', "When enabled, will limit [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors to files that are inside the opened workspace. Only applies when {0} is enabled.", '`#files.autoSave#`'), scope: ConfigurationScope.LANGUAGE_OVERRIDABLE }, 'files.autoSaveWhenNoErrors': { 'type': 'boolean', 'default': false, - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveWhenNoErrors' }, "When enabled, will limit [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors to files that have no errors reported in them at the time the auto save is triggered. Only applies when `#files.autoSave#` is enabled."), + 'markdownDescription': nls.localize('autoSaveWhenNoErrors', "When enabled, will limit [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors to files that have no errors reported in them at the time the auto save is triggered. Only applies when {0} is enabled.", '`#files.autoSave#`'), scope: ConfigurationScope.LANGUAGE_OVERRIDABLE }, 'files.watcherExclude': { diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 1cb4d5031b9..be030fe58e4 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -261,7 +261,7 @@ export class OpenEditorsView extends ViewPane { this.readonlyEditorFocusedContext = OpenEditorsReadonlyEditorContext.bindTo(this.contextKeyService); this._register(this.list.onContextMenu(e => this.onListContextMenu(e))); - this.list.onDidChangeFocus(e => { + this._register(this.list.onDidChangeFocus(e => { this.resourceContext.reset(); this.groupFocusedContext.reset(); this.dirtyEditorFocusedContext.reset(); @@ -275,7 +275,7 @@ export class OpenEditorsView extends ViewPane { } else if (!!element) { this.groupFocusedContext.set(true); } - }); + })); // Open when selecting via keyboard this._register(this.list.onMouseMiddleClick(e => { diff --git a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index f104ba762eb..75b57831d84 100644 --- a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; diff --git a/src/vs/workbench/contrib/files/test/browser/explorerFileNestingTrie.test.ts b/src/vs/workbench/contrib/files/test/browser/explorerFileNestingTrie.test.ts index 2c08f73b54c..66481ab3a23 100644 --- a/src/vs/workbench/contrib/files/test/browser/explorerFileNestingTrie.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/explorerFileNestingTrie.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { PreTrie, ExplorerFileNestingTrie, SufTrie } from 'vs/workbench/contrib/files/common/explorerFileNestingTrie'; -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; const fakeFilenameAttributes = { dirname: 'mydir', basename: '', extname: '' }; diff --git a/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts b/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts index 938fed848a2..13870699b3c 100644 --- a/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isLinux, isWindows, OS } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/path'; diff --git a/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts b/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts index cea1d9c7172..ea30440fa54 100644 --- a/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts b/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts index e1e8db24108..f5070edb179 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { incrementFileName } from 'vs/workbench/contrib/files/browser/fileActions'; diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index ef6adefcdd2..1675b2d4571 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; diff --git a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts index ca865ea076c..94f9a716751 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts index 75e6c3b5cdc..f3d6c8fb198 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { TextFileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/textFileEditorTracker'; import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 4ab19a40890..7d3e4041271 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { registerAction2 } from 'vs/platform/actions/common/actions'; +import { IMenuItem, isIMenuItem, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import * as InlineChatActions from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; -import { INLINE_CHAT_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_CONFIG_TXT_BTNS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, INLINE_CHAT_ID, InlineChatConfigKeys, MENU_INLINE_CHAT_CONTENT_STATUS, MENU_INLINE_CHAT_EXECUTE, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -19,6 +19,12 @@ import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browse import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; import { InlineChatEnabler, InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { CancelAction, SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { localize } from 'vs/nls'; +import { CONTEXT_CHAT_INPUT_HAS_TEXT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; // --- browser @@ -28,6 +34,41 @@ registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, Instant registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors +// --- MENU special --- + +const sendActionMenuItem: IMenuItem = { + group: '0_main', + order: 0, + command: { + id: SubmitAction.ID, + title: localize('edit', "Send"), + }, + when: ContextKeyExpr.and( + CONTEXT_CHAT_INPUT_HAS_TEXT, + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.toNegated(), + CTX_INLINE_CHAT_CONFIG_TXT_BTNS + ), +}; + +MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_CONTENT_STATUS, sendActionMenuItem); +MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, sendActionMenuItem); + +const cancelActionMenuItem: IMenuItem = { + group: '0_main', + order: 0, + command: { + id: CancelAction.ID, + title: localize('cancel', "Stop Request"), + shortTitle: localize('cancelShort', "Stop"), + }, + when: ContextKeyExpr.and( + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, + ), +}; + +MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, cancelActionMenuItem); + +// --- actions --- registerAction2(InlineChatActions.StartSessionAction); registerAction2(InlineChatActions.CloseAction); @@ -36,7 +77,6 @@ registerAction2(InlineChatActions.UnstashSessionAction); registerAction2(InlineChatActions.DiscardHunkAction); registerAction2(InlineChatActions.DiscardAction); registerAction2(InlineChatActions.RerunAction); -registerAction2(InlineChatActions.CancelSessionAction); registerAction2(InlineChatActions.MoveToNextHunk); registerAction2(InlineChatActions.MoveToPreviousHunk); @@ -56,3 +96,47 @@ workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatNotebookC registerWorkbenchContribution2(InlineChatEnabler.Id, InlineChatEnabler, WorkbenchPhase.AfterRestored); AccessibleViewRegistry.register(new InlineChatAccessibleView()); + + +// MARK - Menu Copier +// menu copier that we use for text-button mode. +// When active it filters out the send and cancel actions from the chat menu +class MenuCopier implements IDisposable { + + static Id = 'inlineChat.menuCopier'; + + readonly dispose: () => void; + + constructor(@IConfigurationService configService: IConfigurationService,) { + + const store = new DisposableStore(); + function updateMenu() { + store.clear(); + for (const item of MenuRegistry.getMenuItems(MenuId.ChatExecute)) { + if (configService.getValue(InlineChatConfigKeys.ExpTextButtons) && isIMenuItem(item) && (item.command.id === SubmitAction.ID || item.command.id === CancelAction.ID)) { + continue; + } + store.add(MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_EXECUTE, item)); + } + } + updateMenu(); + const listener = MenuRegistry.onDidChangeMenu(e => { + if (e.has(MenuId.ChatExecute)) { + updateMenu(); + } + }); + const listener2 = configService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(InlineChatConfigKeys.ExpTextButtons)) { + updateMenu(); + } + }); + + this.dispose = () => { + listener.dispose(); + listener2.dispose(); + store.dispose(); + }; + } +} + +registerWorkbenchContribution2(MenuCopier.Id, MenuCopier, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts index 91a719e1999..797dae2825d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts @@ -11,6 +11,8 @@ import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/access import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; export class InlineChatAccessibleView implements IAccessibleViewImplentation { readonly priority = 100; @@ -35,7 +37,7 @@ export class InlineChatAccessibleView implements IAccessibleViewImplentation { return { id: AccessibleViewProviderId.InlineChat, verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, - provideContent(): string { return responseContent; }, + provideContent(): string { return renderMarkdownAsPlaintext(new MarkdownString(responseContent), true); }, onClose() { controller.focus(); }, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index b7c33b6df15..2009001a7ea 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/em import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_HAS_STASHED_SESSION, ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET, ACTION_TOGGLE_DIFF, ACTION_REGENERATE_RESPONSE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, MENU_INLINE_CHAT_CONTENT_STATUS, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize, localize2 } from 'vs/nls'; import { Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -29,7 +29,6 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ILogService } from 'vs/platform/log/common/log'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { CONTEXT_CHAT_REQUEST_IN_PROGRESS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); @@ -80,7 +79,7 @@ export class StartSessionAction extends EditorAction2 { let options: InlineChatRunOptions | undefined; const arg = _args[0]; - if (arg && InlineChatRunOptions.isInteractiveEditorOptions(arg)) { + if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { options = arg; } InlineChatController.get(editor)?.run({ ...options }); @@ -228,27 +227,6 @@ export class FocusInlineChat extends EditorAction2 { } } -export class DiscardHunkAction extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineChat.discardHunkChange', - title: localize('discard', 'Discard'), - icon: Codicon.clearAll, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty), CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live)), - group: '0_main', - order: 3 - } - }); - } - - async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - return ctrl.discardHunk(); - } -} export class DiscardAction extends AbstractInlineChatAction { @@ -271,33 +249,6 @@ export class DiscardAction extends AbstractInlineChatAction { } } -export class ToggleDiffForChange extends AbstractInlineChatAction { - - constructor() { - super({ - id: ACTION_TOGGLE_DIFF, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF), - title: localize2('showChanges', 'Toggle Changes'), - icon: Codicon.diffSingle, - toggled: { - condition: CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, - }, - menu: [ - { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '1_main', - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF), - order: 10, - } - ] - }); - } - - override runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController): void { - ctrl.toggleDiff(); - } -} - export class AcceptChanges extends AbstractInlineChatAction { constructor() { @@ -313,10 +264,13 @@ export class AcceptChanges extends AbstractInlineChatAction { primary: KeyMod.CtrlCmd | KeyCode.Enter, }], menu: { - when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), id: MENU_INLINE_CHAT_WIDGET_STATUS, group: '0_main', - order: 0 + order: 1, + when: ContextKeyExpr.and( + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.toNegated(), + CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits) + ), } }); } @@ -326,32 +280,72 @@ export class AcceptChanges extends AbstractInlineChatAction { } } -export class CancelSessionAction extends AbstractInlineChatAction { +export class DiscardHunkAction extends AbstractInlineChatAction { constructor() { super({ - id: 'inlineChat.cancel', - title: localize('cancel', 'Cancel'), + id: 'inlineChat.discardHunkChange', + title: localize('discard', 'Discard'), icon: Codicon.clearAll, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Preview)), - keybinding: { - weight: KeybindingWeight.EditorContrib - 1, - primary: KeyCode.Escape - }, + precondition: CTX_INLINE_CHAT_VISIBLE, menu: { id: MENU_INLINE_CHAT_WIDGET_STATUS, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Preview), CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), group: '0_main', - order: 3 + order: 2, + when: ContextKeyExpr.and( + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), + CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits), + CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live) + ), + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape, + when: CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits) } }); } async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - ctrl.cancelSession(); + return ctrl.discardHunk(); } } +export class RerunAction extends AbstractInlineChatAction { + constructor() { + super({ + id: ACTION_REGENERATE_RESPONSE, + title: localize2('chat.rerun.label', "Rerun Request"), + shortTitle: localize('rerun', 'Rerun'), + f1: false, + icon: Codicon.refresh, + precondition: CTX_INLINE_CHAT_VISIBLE, + menu: { + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: '0_main', + order: 5, + when: ContextKeyExpr.and( + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), + CTX_INLINE_CHAT_RESPONSE_TYPE.notEqualsTo(InlineChatResponseType.None) + ) + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyR + } + }); + } + + override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { + const chatService = accessor.get(IChatService); + const model = ctrl.chatWidget.viewModel?.model; + + const lastRequest = model?.getRequests().at(-1); + if (lastRequest) { + await chatService.resendRequest(lastRequest, { noCommandDetection: false, attempt: lastRequest.attempt + 1, location: ctrl.chatWidget.location }); + } + } +} export class CloseAction extends AbstractInlineChatAction { @@ -362,15 +356,25 @@ export class CloseAction extends AbstractInlineChatAction { icon: Codicon.close, precondition: CTX_INLINE_CHAT_VISIBLE, keybinding: { - weight: KeybindingWeight.EditorContrib - 1, + weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Escape, - when: CTX_INLINE_CHAT_USER_DID_EDIT.negate() }, - menu: { - id: MENU_INLINE_CHAT_WIDGET, - group: 'navigation', + menu: [{ + id: MENU_INLINE_CHAT_CONTENT_STATUS, + group: '0_main', order: 10, - } + }, { + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: '0_main', + order: 1, + when: ContextKeyExpr.and( + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), + ContextKeyExpr.or( + CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.Messages), + CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Preview) + ) + ), + }] }); } @@ -387,6 +391,11 @@ export class ConfigureInlineChatAction extends AbstractInlineChatAction { icon: Codicon.settingsGear, precondition: CTX_INLINE_CHAT_VISIBLE, f1: true, + menu: { + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: 'zzz', + order: 5 + } }); } @@ -478,10 +487,23 @@ export class ViewInChatAction extends AbstractInlineChatAction { title: localize('viewInChat', 'View in Chat'), icon: Codicon.commentDiscussion, precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET, - group: 'navigation', - order: 5 + menu: [{ + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: 'more', + order: 1, + when: CTX_INLINE_CHAT_RESPONSE_TYPE.notEqualsTo(InlineChatResponseType.Messages) + }, { + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: '0_main', + order: 1, + when: ContextKeyExpr.and( + CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.Messages), + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate() + ) + }], + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.DownArrow, } }); } @@ -490,29 +512,27 @@ export class ViewInChatAction extends AbstractInlineChatAction { } } -export class RerunAction extends AbstractInlineChatAction { +export class ToggleDiffForChange extends AbstractInlineChatAction { + constructor() { super({ - id: ACTION_REGENERATE_RESPONSE, - title: localize2('chat.rerun.label', "Rerun Request"), - f1: false, - icon: Codicon.refresh, - precondition: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), + id: ACTION_TOGGLE_DIFF, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF), + title: localize2('showChanges', 'Toggle Changes'), + icon: Codicon.diffSingle, + toggled: { + condition: CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, + }, menu: { id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 5, + group: 'zzz', + when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live)), + order: 1, } }); } - override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - const chatService = accessor.get(IChatService); - const model = ctrl.chatWidget.viewModel?.model; - - const lastRequest = model?.getRequests().at(-1); - if (lastRequest) { - await chatService.resendRequest(lastRequest, { noCommandDetection: false, attempt: lastRequest.attempt + 1, location: ctrl.chatWidget.location }); - } + override runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController): void { + ctrl.toggleDiff(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts index aa2411d731e..20004ab3eb2 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts @@ -10,9 +10,8 @@ import { IDimension } from 'vs/editor/common/core/dimension'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IPosition, Position } from 'vs/editor/common/core/position'; -import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { inlineChatBackground } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { inlineChatBackground, InlineChatConfigKeys, MENU_INLINE_CHAT_CONTENT_STATUS, MENU_INLINE_CHAT_EXECUTE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; @@ -23,6 +22,10 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ScrollType } from 'vs/editor/common/editorCommon'; +import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { TextOnlyMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class InlineChatContentWidget implements IContentWidget { @@ -32,7 +35,7 @@ export class InlineChatContentWidget implements IContentWidget { private readonly _store = new DisposableStore(); private readonly _domNode = document.createElement('div'); private readonly _inputContainer = document.createElement('div'); - private readonly _messageContainer = document.createElement('div'); + private readonly _toolbarContainer = document.createElement('div'); private _position?: IPosition; @@ -50,6 +53,7 @@ export class InlineChatContentWidget implements IContentWidget { private readonly _editor: ICodeEditor, @IInstantiationService instaService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService configurationService: IConfigurationService ) { this._defaultChatModel = this._store.add(instaService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor)); @@ -69,12 +73,13 @@ export class InlineChatContentWidget implements IContentWidget { { defaultElementHeight: 32, editorOverflowWidgetsDomNode: _editor.getOverflowWidgetsDomNode(), - renderStyle: 'compact', + renderStyle: 'minimal', renderInputOnTop: true, renderFollowups: true, supportsFileReferences: false, menus: { - telemetrySource: 'inlineChat-content' + telemetrySource: 'inlineChat-content', + executeToolbar: MENU_INLINE_CHAT_EXECUTE, }, filter: _item => false }, @@ -88,22 +93,34 @@ export class InlineChatContentWidget implements IContentWidget { this._store.add(this._widget); this._widget.render(this._inputContainer); this._widget.setModel(this._defaultChatModel, {}); - this._store.add(this._widget.inputEditor.onDidContentSizeChange(() => _editor.layoutContentWidget(this))); + this._store.add(this._widget.onDidChangeContentHeight(() => _editor.layoutContentWidget(this))); this._domNode.tabIndex = -1; this._domNode.className = 'inline-chat-content-widget interactive-session'; this._domNode.appendChild(this._inputContainer); - this._messageContainer.classList.add('hidden', 'message'); - this._domNode.appendChild(this._messageContainer); + this._toolbarContainer.classList.add('toolbar'); + if (configurationService.getValue(InlineChatConfigKeys.ExpTextButtons)) { + this._toolbarContainer.style.display = 'inherit'; + this._domNode.style.paddingBottom = '4px'; + } + this._domNode.appendChild(this._toolbarContainer); + const toolbar = this._store.add(scopedInstaService.createInstance(MenuWorkbenchToolBar, this._toolbarContainer, MENU_INLINE_CHAT_CONTENT_STATUS, { + actionViewItemProvider: action => action instanceof MenuItemAction ? instaService.createInstance(TextOnlyMenuEntryActionViewItem, action, { conversational: true }) : undefined, + toolbarOptions: { primaryGroup: '0_main' }, + icon: false, + label: true, + })); + + this._store.add(toolbar.onDidChangeMenuItems(() => { + this._domNode.classList.toggle('contents', toolbar.getItemsLength() > 1); + })); const tracker = dom.trackFocus(this._domNode); this._store.add(tracker.onDidBlur(() => { - if (this._visible - // && !"ON" - ) { + if (this._visible && this._widget.inputEditor.getModel()?.getValueLength() === 0) { this._onDidBlur.fire(); } })); @@ -137,7 +154,7 @@ export class InlineChatContentWidget implements IContentWidget { const maxHeight = this._widget.input.inputEditor.getOption(EditorOption.lineHeight) * 5; const inputEditorHeight = this._widget.contentHeight; - this._widget.layout(Math.min(maxHeight, inputEditorHeight), 360); + this._widget.layout(Math.min(maxHeight, inputEditorHeight), 390); // const actualHeight = this._widget.inputPartHeight; // return new dom.Dimension(width, actualHeight); @@ -171,7 +188,6 @@ export class InlineChatContentWidget implements IContentWidget { this._focusNext = true; this._editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(position), ScrollType.Immediate); - this._widget.inputEditor.setValue(''); const wordInfo = this._editor.getModel()?.getWordAtPosition(position); @@ -185,6 +201,7 @@ export class InlineChatContentWidget implements IContentWidget { if (this._visible) { this._visible = false; this._editor.removeContentWidget(this); + this._widget.inputEditor.setValue(''); this._widget.saveState(); this._widget.setVisible(false); } @@ -192,16 +209,6 @@ export class InlineChatContentWidget implements IContentWidget { setSession(session: Session): void { this._widget.setModel(session.chatModel, {}); - this._widget.setInputPlaceholder(session.session.placeholder ?? ''); - this._updateMessage(session.session.message ?? ''); - } - - private _updateMessage(message: string) { - if (message) { - const renderedMessage = renderLabelWithIcons(message); - dom.reset(this._messageContainer, ...renderedMessage); - } - this._messageContainer.style.display = message ? 'inherit' : 'none'; - this._editor.layoutContentWidget(this); + this._widget.setInputPlaceholder(session.agent.description ?? ''); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 66ab95eacdd..0befb284dbf 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -35,7 +35,7 @@ import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionPrompt } f import { IInlineChatSessionService } from './inlineChatSessionService'; import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; import { InlineChatZoneWidget } from './inlineChatZoneWidget'; -import { CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { StashedSession } from './inlineChatSession'; import { IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model'; import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget'; @@ -50,6 +50,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { isEqual } from 'vs/base/common/resources'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { escapeRegExpCharacters } from 'vs/base/common/strings'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', @@ -82,7 +83,7 @@ export abstract class InlineChatRunOptions { position?: IPosition; withIntentDetection?: boolean; - static isInteractiveEditorOptions(options: any): options is InlineChatRunOptions { + static isInlineChatRunOptions(options: any): options is InlineChatRunOptions { const { initialSelection, initialRange, message, autoSend, position, existingSession } = options; if ( typeof message !== 'undefined' && typeof message !== 'string' @@ -106,14 +107,13 @@ export class InlineChatController implements IEditorContribution { private _isDisposed: boolean = false; private readonly _store = new DisposableStore(); - // private readonly _input: Lazy; - // private readonly _zone: Lazy; private readonly _ui: Lazy<{ content: InlineChatContentWidget; zone: InlineChatZoneWidget }>; private readonly _ctxVisible: IContextKey; - private readonly _ctxResponseTypes: IContextKey; + private readonly _ctxResponseType: IContextKey; private readonly _ctxUserDidEdit: IContextKey; + private readonly _ctxRequestInProgress: IContextKey; private _messages = this._store.add(new Emitter()); @@ -150,7 +150,8 @@ export class InlineChatController implements IEditorContribution { ) { this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); this._ctxUserDidEdit = CTX_INLINE_CHAT_USER_DID_EDIT.bindTo(contextKeyService); - this._ctxResponseTypes = CTX_INLINE_CHAT_RESPONSE_TYPES.bindTo(contextKeyService); + this._ctxResponseType = CTX_INLINE_CHAT_RESPONSE_TYPE.bindTo(contextKeyService); + this._ctxRequestInProgress = CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService); this._ui = new Lazy(() => { let location = ChatAgentLocation.Editor; @@ -304,7 +305,6 @@ export class InlineChatController implements IEditorContribution { if (m === Message.ACCEPT_INPUT) { // user accepted the input before having a session options.autoSend = true; - this._ui.value.zone.widget.updateProgress(true); this._ui.value.zone.widget.updateInfo(localize('welcome.2', "Getting ready...")); } else { createSessionCts.cancel(); @@ -384,12 +384,9 @@ export class InlineChatController implements IEditorContribution { this._ui.value.content.setSession(this._session); // this._ui.value.zone.widget.updateSlashCommands(this._session.session.slashCommands ?? []); this._updatePlaceholder(); - const message = this._session.session.message ?? localize('welcome.1', "AI-generated code may be incorrect"); - - - this._ui.value.zone.widget.updateInfo(message); this._showWidget(!this._session.chatModel.hasRequests); + this._ui.value.zone.widget.updateToolbar(true); this._sessionStore.add(this._editor.onDidChangeModel((e) => { const msg = this._session?.chatModel.hasRequests @@ -428,40 +425,16 @@ export class InlineChatController implements IEditorContribution { })); this._sessionStore.add(this._session.chatModel.onDidChange(async e => { - if (e.kind === 'addRequest' && e.request.response) { - this._ui.value.zone.widget.updateProgress(true); - - const listener = e.request.response.onDidChange(() => { - - if (e.request.response?.isCanceled || e.request.response?.isComplete) { - this._ui.value.zone.widget.updateProgress(false); - listener.dispose(); - } - }); - } else if (e.kind === 'removeRequest') { + if (e.kind === 'removeRequest') { // TODO@jrieken there is still some work left for when a request "in the middle" // is removed. We will undo all changes till that point but not remove those // later request - const exchange = this._session!.exchanges.find(candidate => candidate.prompt.request.id === e.requestId); - if (exchange && this._editor.hasModel()) { - // undo till this point - this._session!.hunkData.ignoreTextModelNChanges = true; - try { - - const model = this._editor.getModel(); - const targetAltVersion = exchange.prompt.modelAltVersionId; - while (targetAltVersion < model.getAlternativeVersionId() && model.canUndo()) { - await model.undo(); - } - } finally { - this._session!.hunkData.ignoreTextModelNChanges = false; - } - } + await this._session!.undoChangesUntil(e.requestId); } })); // #region DEBT - // DEBT@jrieken + // DEBT@jrieken https://github.com/microsoft/vscode/issues/218819 // REMOVE when agents are adopted this._sessionStore.add(this._languageFeatureService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { _debugDisplayName: 'inline chat commands', @@ -470,7 +443,7 @@ export class InlineChatController implements IEditorContribution { if (position.lineNumber !== 1) { return undefined; } - if (!this._session || !this._session.session.slashCommands) { + if (!this._session || !this._session.agent.slashCommands) { return undefined; } const widget = this._chatWidgetService.getWidgetByInputUri(model.uri); @@ -479,12 +452,12 @@ export class InlineChatController implements IEditorContribution { } const result: CompletionList = { suggestions: [], incomplete: false }; - for (const command of this._session.session.slashCommands) { + for (const command of this._session.agent.slashCommands) { const withSlash = `/${command.name}`; result.suggestions.push({ label: { label: withSlash, description: command.description ?? '' }, kind: CompletionItemKind.Text, - insertText: withSlash, + insertText: `${withSlash} `, range: Range.fromPositions(new Position(1, 1), position), }); } @@ -496,19 +469,15 @@ export class InlineChatController implements IEditorContribution { const updateSlashDecorations = (collection: IEditorDecorationsCollection, model: ITextModel) => { const newDecorations: IModelDeltaDecoration[] = []; - for (const command of (this._session?.session.slashCommands ?? []).sort((a, b) => b.name.length - a.name.length)) { + for (const command of (this._session?.agent.slashCommands ?? []).sort((a, b) => b.name.length - a.name.length)) { const withSlash = `/${command.name}`; const firstLine = model.getLineContent(1); - if (firstLine.startsWith(withSlash)) { + if (firstLine.match(new RegExp(`^${escapeRegExpCharacters(withSlash)}(\\s|$)`))) { newDecorations.push({ range: new Range(1, 1, 1, withSlash.length + 1), options: { description: 'inline-chat-slash-command', inlineClassName: 'inline-chat-slash-command', - after: { - // Force some space between slash command and placeholder - content: ' ' - } } }); @@ -613,9 +582,6 @@ export class InlineChatController implements IEditorContribution { return State.WAIT_FOR_INPUT; } - const input = request.message.text; - this._ui.value.zone.widget.value = input; - this._session.addInput(new SessionPrompt(request, this._editor.getModel()!.getAlternativeVersionId())); return State.SHOW_REQUEST; @@ -626,6 +592,8 @@ export class InlineChatController implements IEditorContribution { assertType(this._session); assertType(this._session.chatModel.requestInProgress); + this._ctxRequestInProgress.set(true); + const { chatModel } = this._session; const request: IChatRequestModel | undefined = chatModel.getRequests().at(-1); @@ -633,7 +601,7 @@ export class InlineChatController implements IEditorContribution { assertType(request.response); this._showWidget(false); - this._ui.value.zone.widget.value = request.message.text; + // this._ui.value.zone.widget.value = request.message.text; this._ui.value.zone.widget.selectAll(false); this._ui.value.zone.widget.updateInfo(''); @@ -690,6 +658,8 @@ export class InlineChatController implements IEditorContribution { // apply edits const handleResponse = () => { + this._updateCtxResponseType(); + if (!localEditGroup) { localEditGroup = response.response.value.find(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)); } @@ -747,16 +717,19 @@ export class InlineChatController implements IEditorContribution { await responsePromise.p; await progressiveEditsQueue.whenIdle(); + + if (response.isCanceled) { + // + await this._session.undoChangesUntil(response.requestId); + } + store.dispose(); - // todo@jrieken we can likely remove 'trackEdit' const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { computeMoves: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, ignoreTrimWhitespace: false }, 'advanced'); this._session.wholeRange.fixup(diff?.changes ?? []); + await this._session.hunkData.recompute(editState, diff); - await this._session.hunkData.recompute(editState); - - this._ui.value.zone.widget.updateToolbar(true); - this._ui.value.zone.widget.updateProgress(false); + this._ctxRequestInProgress.set(false); return next; } @@ -767,20 +740,6 @@ export class InlineChatController implements IEditorContribution { const { response } = this._session.lastExchange!; - let responseTypes: InlineChatResponseTypes | undefined; - for (const request of this._session.chatModel.getRequests()) { - if (!request.response) { - continue; - } - const thisType = asInlineChatResponseType(request.response.response); - if (responseTypes === undefined) { - responseTypes = thisType; - } else if (responseTypes !== thisType) { - responseTypes = InlineChatResponseTypes.Mixed; - break; - } - } - this._ctxResponseTypes.set(responseTypes); let newPosition: Position | undefined; @@ -800,9 +759,20 @@ export class InlineChatController implements IEditorContribution { } else if (response instanceof ReplyResponse) { // real response -> complex... this._ui.value.zone.widget.updateStatus(''); - this._ui.value.zone.widget.updateToolbar(true); - newPosition = await this._strategy.renderChanges(response); + const position = await this._strategy.renderChanges(); + if (position) { + // if the selection doesn't start far off we keep the widget at its current position + // because it makes reading this nicer + const selection = this._editor.getSelection(); + if (selection?.containsPosition(position)) { + if (position.lineNumber - selection.startLineNumber > 8) { + newPosition = position; + } + } else { + newPosition = position; + } + } } this._showWidget(false, newPosition); @@ -846,7 +816,7 @@ export class InlineChatController implements IEditorContribution { this._sessionStore.clear(); // only stash sessions that were not unstashed, not "empty", and not interacted with - const shouldStash = !this._session.isUnstashed && !!this._session.lastExchange && this._session.hunkData.size === this._session.hunkData.pending; + const shouldStash = !this._session.isUnstashed && this._session.chatModel.hasRequests && this._session.hunkData.size === this._session.hunkData.pending; let undoCancelEdits: IValidEditOperation[] = []; try { undoCancelEdits = this._strategy.cancel(); @@ -875,6 +845,7 @@ export class InlineChatController implements IEditorContribution { private _showWidget(initialRender: boolean = false, position?: Position) { assertType(this._editor.hasModel()); + this._ctxVisible.set(true); let widgetPosition: Position; if (position) { @@ -892,7 +863,7 @@ export class InlineChatController implements IEditorContribution { widgetPosition = this._editor.getSelection().getStartPosition().delta(-1); } - if (this._session && !position && (this._session.hasChangedText || this._session.lastExchange)) { + if (this._session && !position && (this._session.hasChangedText || this._session.chatModel.hasRequests)) { widgetPosition = this._session.wholeRange.value.getStartPosition().delta(-1); } @@ -902,13 +873,6 @@ export class InlineChatController implements IEditorContribution { } else if (initialRender) { const selection = this._editor.getSelection(); widgetPosition = selection.getStartPosition(); - // TODO@jrieken we are not ready for this - // widgetPosition = selection.getEndPosition(); - // if (Range.spansMultipleLines(selection) && widgetPosition.column === 1) { - // // selection ends on "nothing" -> move up to match the - // // rendered/visible part of the selection - // widgetPosition = this._editor.getModel().validatePosition(widgetPosition.delta(-1, Number.MAX_SAFE_INTEGER)); - // } this._ui.value.content.show(widgetPosition); } else { @@ -919,11 +883,6 @@ export class InlineChatController implements IEditorContribution { } } - if (this._session && this._ui.rawValue?.zone) { - this._ui.rawValue?.zone.updateBackgroundColor(widgetPosition, this._session.wholeRange.value); - } - - this._ctxVisible.set(true); return widgetPosition; } @@ -941,6 +900,31 @@ export class InlineChatController implements IEditorContribution { } } + private _updateCtxResponseType(): void { + + if (!this._session) { + this._ctxResponseType.set(InlineChatResponseType.None); + return; + } + + const hasLocalEdit = (response: IResponse): boolean => { + return response.value.some(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)); + }; + + let responseType = InlineChatResponseType.None; + for (const request of this._session.chatModel.getRequests()) { + if (!request.response) { + continue; + } + responseType = InlineChatResponseType.Messages; + if (hasLocalEdit(request.response.response)) { + responseType = InlineChatResponseType.MessagesAndEdits; + break; // no need to check further + } + } + this._ctxResponseType.set(responseType); + } + private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined, undoStopBefore: boolean) { assertType(this._session); assertType(this._strategy); @@ -962,7 +946,6 @@ export class InlineChatController implements IEditorContribution { }; this._inlineChatSavingService.markChanged(this._session); - this._session.wholeRange.trackEdits(editOperations); if (opts) { await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts, undoStopBefore); } else { @@ -977,7 +960,7 @@ export class InlineChatController implements IEditorContribution { } private _getPlaceholderText(): string { - return this._forcedPlaceholder ?? this._session?.session.placeholder ?? ''; + return this._forcedPlaceholder ?? this._session?.agent.description ?? ''; } // ---- controller API @@ -1069,10 +1052,10 @@ export class InlineChatController implements IEditorContribution { } acceptSession(): void { - if (this._session?.lastExchange?.response instanceof ReplyResponse && this._session?.lastExchange?.response.chatResponse) { - const response = this._session?.lastExchange?.response.chatResponse; + const response = this._session?.chatModel.getRequests().at(-1)?.response; + if (response) { this._chatService.notifyUserAction({ - sessionId: this._session.chatModel.sessionId, + sessionId: response.session.sessionId, requestId: response.requestId, agentId: response.agent?.id, result: response.result, @@ -1094,11 +1077,10 @@ export class InlineChatController implements IEditorContribution { } async cancelSession() { - - if (this._session?.lastExchange?.response instanceof ReplyResponse && this._session?.lastExchange?.response.chatResponse) { - const response = this._session?.lastExchange?.response.chatResponse; + const response = this._session?.chatModel.getRequests().at(-1)?.response; + if (response) { this._chatService.notifyUserAction({ - sessionId: this._session.chatModel.sessionId, + sessionId: response.session.sessionId, requestId: response.requestId, agentId: response.agent?.id, result: response.result, @@ -1151,25 +1133,3 @@ async function moveToPanelChat(accessor: ServicesAccessor, model: ChatModel | un widget.focusLastMessage(); } } - -function asInlineChatResponseType(response: IResponse): InlineChatResponseTypes { - let result: InlineChatResponseTypes | undefined; - for (const item of response.value) { - let thisType: InlineChatResponseTypes; - switch (item.kind) { - case 'textEditGroup': - thisType = InlineChatResponseTypes.OnlyEdits; - break; - case 'markdownContent': - default: - thisType = InlineChatResponseTypes.OnlyMessages; - break; - } - if (result === undefined) { - result = thisType; - } else if (result !== thisType) { - return InlineChatResponseTypes.Mixed; - } - } - return result ?? InlineChatResponseTypes.Empty; -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget.ts deleted file mode 100644 index eca335cb375..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget.ts +++ /dev/null @@ -1,256 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Dimension, h } from 'vs/base/browser/dom'; -import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Range } from 'vs/editor/common/core/range'; -import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; -import * as editorColorRegistry from 'vs/editor/common/core/editorColorRegistry'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { INLINE_CHAT_ID, inlineChatRegionHighlight } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { Position } from 'vs/editor/common/core/position'; -import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { ResourceLabel } from 'vs/workbench/browser/labels'; -import { FileKind } from 'vs/platform/files/common/files'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { SaveReason, SideBySideEditor } from 'vs/workbench/common/editor'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IAction, toAction } from 'vs/base/common/actions'; -import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; -import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { Codicon } from 'vs/base/common/codicons'; -import { TAB_ACTIVE_MODIFIED_BORDER } from 'vs/workbench/common/theme'; -import { localize } from 'vs/nls'; -import { Event } from 'vs/base/common/event'; - -export class InlineChatFileCreatePreviewWidget extends ZoneWidget { - - private static TitleHeight = 35; - - private readonly _elements = h('div.inline-chat-newfile-widget@domNode', [ - h('div.title@title', [ - h('span.name.show-file-icons@name'), - h('span.detail@detail'), - ]), - h('div.editor@editor'), - ]); - - private readonly _name: ResourceLabel; - private readonly _previewEditor: ICodeEditor; - private readonly _previewStore = new MutableDisposable(); - private readonly _buttonBar: ButtonBarWidget; - private _dim: Dimension | undefined; - - constructor( - parentEditor: ICodeEditor, - @IInstantiationService instaService: IInstantiationService, - @IThemeService themeService: IThemeService, - @ITextModelService private readonly _textModelResolverService: ITextModelService, - @IEditorService private readonly _editorService: IEditorService, - ) { - super(parentEditor, { - showArrow: false, - showFrame: true, - frameColor: colorRegistry.asCssVariable(TAB_ACTIVE_MODIFIED_BORDER), - frameWidth: 1, - isResizeable: true, - isAccessible: true, - showInHiddenAreas: true, - ordinal: 10000 + 2 - }); - super.create(); - - this._name = instaService.createInstance(ResourceLabel, this._elements.name, { supportIcons: true }); - this._elements.detail.appendChild(renderIcon(Codicon.circleFilled)); - - const contributions = EditorExtensionsRegistry - .getEditorContributions() - .filter(c => c.id !== INLINE_CHAT_ID); - - this._previewEditor = instaService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, { - scrollBeyondLastLine: false, - stickyScroll: { enabled: false }, - minimap: { enabled: false }, - scrollbar: { alwaysConsumeMouseWheel: false, useShadows: true, ignoreHorizontalScrollbarInContentHeight: true, }, - }, { isSimpleWidget: true, contributions }, parentEditor); - - const doStyle = () => { - const theme = themeService.getColorTheme(); - const overrides: [target: string, source: string][] = [ - [colorRegistry.editorBackground, inlineChatRegionHighlight], - [editorColorRegistry.editorGutter, inlineChatRegionHighlight], - ]; - - for (const [target, source] of overrides) { - const value = theme.getColor(source); - if (value) { - this._elements.domNode.style.setProperty(colorRegistry.asCssVariableName(target), String(value)); - } - } - }; - doStyle(); - this._disposables.add(themeService.onDidColorThemeChange(doStyle)); - - this._buttonBar = instaService.createInstance(ButtonBarWidget); - this._elements.title.appendChild(this._buttonBar.domNode); - } - - override dispose(): void { - this._name.dispose(); - this._buttonBar.dispose(); - this._previewEditor.dispose(); - this._previewStore.dispose(); - super.dispose(); - } - - protected override _fillContainer(container: HTMLElement): void { - container.appendChild(this._elements.domNode); - } - - override show(): void { - throw new Error('Use showFileCreation'); - } - - async showCreation(where: Position, untitledTextModel: IUntitledTextEditorModel): Promise { - - const store = new DisposableStore(); - this._previewStore.value = store; - - this._name.element.setFile(untitledTextModel.resource, { - fileKind: FileKind.FILE, - fileDecorations: { badges: true, colors: true } - }); - - const actionSave = toAction({ - id: '1', - label: localize('save', "Create"), - run: () => untitledTextModel.save({ reason: SaveReason.EXPLICIT }) - }); - const actionSaveAs = toAction({ - id: '2', - label: localize('saveAs', "Create As"), - run: async () => { - const ids = this._editorService.findEditors(untitledTextModel.resource, { supportSideBySide: SideBySideEditor.ANY }); - await this._editorService.save(ids.slice(), { saveAs: true, reason: SaveReason.EXPLICIT }); - } - }); - - this._buttonBar.update([ - [actionSave, actionSaveAs], - [(toAction({ id: '3', label: localize('discard', "Discard"), run: () => untitledTextModel.revert() }))] - ]); - - store.add(Event.any( - untitledTextModel.onDidRevert, - untitledTextModel.onDidSave, - untitledTextModel.onDidChangeDirty, - untitledTextModel.onWillDispose - )(() => this.hide())); - - await untitledTextModel.resolve(); - - const ref = await this._textModelResolverService.createModelReference(untitledTextModel.resource); - store.add(ref); - - const model = ref.object.textEditorModel; - this._previewEditor.setModel(model); - - const lineHeight = this.editor.getOption(EditorOption.lineHeight); - - this._elements.title.style.height = `${InlineChatFileCreatePreviewWidget.TitleHeight}px`; - const titleHightInLines = InlineChatFileCreatePreviewWidget.TitleHeight / lineHeight; - - const maxLines = Math.max(4, Math.floor((this.editor.getLayoutInfo().height / lineHeight) * .33)); - const lines = Math.min(maxLines, model.getLineCount()); - - super.show(where, titleHightInLines + lines); - } - - override hide(): void { - this._previewStore.clear(); - super.hide(); - } - - // --- layout - - protected override revealRange(range: Range, isLastLine: boolean): void { - // ignore - } - - protected override _onWidth(widthInPixel: number): void { - if (this._dim) { - this._doLayout(this._dim.height, widthInPixel); - } - } - - protected override _doLayout(heightInPixel: number, widthInPixel: number): void { - - const { lineNumbersLeft } = this.editor.getLayoutInfo(); - this._elements.title.style.marginLeft = `${lineNumbersLeft}px`; - - const newDim = new Dimension(widthInPixel, heightInPixel); - if (!Dimension.equals(this._dim, newDim)) { - this._dim = newDim; - this._previewEditor.layout(this._dim.with(undefined, this._dim.height - InlineChatFileCreatePreviewWidget.TitleHeight)); - } - } -} - - -class ButtonBarWidget { - - private readonly _domNode = h('div.buttonbar-widget'); - private readonly _buttonBar: ButtonBar; - private readonly _store = new DisposableStore(); - - constructor( - @IContextMenuService private _contextMenuService: IContextMenuService, - ) { - this._buttonBar = new ButtonBar(this.domNode); - - } - - update(allActions: IAction[][]): void { - this._buttonBar.clear(); - let secondary = false; - for (const actions of allActions) { - let btn: IButton; - const [first, ...rest] = actions; - if (!first) { - continue; - } else if (rest.length === 0) { - // single action - btn = this._buttonBar.addButton({ ...defaultButtonStyles, secondary }); - } else { - btn = this._buttonBar.addButtonWithDropdown({ - ...defaultButtonStyles, - addPrimaryActionToDropdown: false, - actions: rest, - contextMenuProvider: this._contextMenuService - }); - } - btn.label = first.label; - this._store.add(btn.onDidClick(() => first.run())); - secondary = true; - } - } - - dispose(): void { - this._buttonBar.dispose(); - this._store.dispose(); - } - - get domNode() { - return this._domNode.root; - } -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index b360c83a016..4d18fe88fce 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -5,10 +5,9 @@ import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; -import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { TextEdit } from 'vs/editor/common/languages'; import { IIdentifiedSingleEditOperation, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation, TrackedRangeStickiness } from 'vs/editor/common/model'; -import { EditMode, IInlineChatSession, CTX_INLINE_CHAT_HAS_STASHED_SESSION, IInlineChatResponse } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { EditMode, CTX_INLINE_CHAT_HAS_STASHED_SESSION } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -34,6 +33,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { ChatModel, IChatRequestModel, IChatResponseModel, IChatTextEditGroupState } from 'vs/workbench/contrib/chat/common/chatModel'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IChatAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IDocumentDiff } from 'vs/editor/common/diff/documentDiffProvider'; export type TelemetryData = { @@ -89,15 +89,6 @@ export class SessionWholeRange { } } - trackEdits(edits: ISingleEditOperation[]): void { - const newDeco: IModelDeltaDecoration[] = []; - for (const edit of edits) { - newDeco.push({ range: edit.range, options: SessionWholeRange._options }); - } - this._decorationIds.push(...this._textModel.deltaDecorations([], newDeco)); - this._onDidChange.fire(this); - } - fixup(changes: readonly DetailedLineRangeMapping[]): void { const newDeco: IModelDeltaDecoration[] = []; @@ -139,7 +130,7 @@ export class Session { private _lastInput: SessionPrompt | undefined; private _isUnstashed: boolean = false; - private readonly _exchange: SessionExchange[] = []; + private readonly _exchanges: SessionExchange[] = []; private readonly _startTime = new Date(); private readonly _teldata: TelemetryData; @@ -156,11 +147,10 @@ export class Session { */ readonly textModel0: ITextModel, /** - * The document into which AI edits went, when live this is `targetUri` otherwise it is a temporary document + * The model of the editor */ readonly textModelN: ITextModel, readonly agent: IChatAgent, - readonly session: IInlineChatSession, readonly wholeRange: SessionWholeRange, readonly hunkData: HunkData, readonly chatModel: ChatModel, @@ -201,17 +191,35 @@ export class Session { addExchange(exchange: SessionExchange): void { this._isUnstashed = false; - const newLen = this._exchange.push(exchange); + const newLen = this._exchanges.push(exchange); this._teldata.rounds += `${newLen}|`; // this._teldata.responseTypes += `${exchange.response instanceof ReplyResponse ? exchange.response.responseType : InlineChatResponseTypes.Empty}|`; } - get exchanges(): readonly SessionExchange[] { - return this._exchange; + get lastExchange(): SessionExchange | undefined { + return this._exchanges[this._exchanges.length - 1]; } - get lastExchange(): SessionExchange | undefined { - return this._exchange[this._exchange.length - 1]; + async undoChangesUntil(requestId: string): Promise { + const idx = this._exchanges.findIndex(candidate => candidate.prompt.request.id === requestId); + if (idx < 0) { + return false; + } + // undo till this point + this.hunkData.ignoreTextModelNChanges = true; + try { + const targetAltVersion = this._exchanges[idx].prompt.modelAltVersionId; + while (targetAltVersion < this.textModelN.getAlternativeVersionId() && this.textModelN.canUndo()) { + await this.textModelN.undo(); + } + } finally { + this.hunkData.ignoreTextModelNChanges = false; + } + // TODO@jrieken cannot do this yet because some parts still rely on + // exchanges being around... + // // remove this and following exchanges + // this._exchanges.length = idx; + return true; } get hasChangedText(): boolean { @@ -257,14 +265,14 @@ export class Session { asRecording(): Recording { const result: Recording = { - session: this.session, + session: this.chatModel.sessionId, when: this._startTime, exchanges: [] }; - for (const exchange of this._exchange) { + for (const exchange of this._exchanges) { const response = exchange.response; if (response instanceof ReplyResponse) { - result.exchanges.push({ prompt: exchange.prompt.value, res: response.raw }); + result.exchanges.push({ prompt: exchange.prompt.value, res: response.chatResponse }); } } return result; @@ -314,9 +322,7 @@ export class ReplyResponse { readonly untitledTextModel: IUntitledTextEditorModel | undefined; constructor( - readonly raw: IInlineChatResponse, localUri: URI, - readonly modelAltVersionId: number, readonly chatRequest: IChatRequestModel, readonly chatResponse: IChatResponseModel, @ITextFileService private readonly _textFileService: ITextFileService, @@ -324,28 +330,20 @@ export class ReplyResponse { ) { const editsMap = new ResourceMap(); - const edits = ResourceEdit.convert(raw.edits); - - for (const edit of edits) { - if (edit instanceof ResourceFileEdit) { - if (edit.newResource && !edit.oldResource) { - editsMap.set(edit.newResource, []); - if (edit.options.contents) { - console.warn('CONTENT not supported'); + + for (const item of chatResponse.response.value) { + if (item.kind === 'textEditGroup') { + const array = editsMap.get(item.uri); + for (const group of item.edits) { + if (array) { + array.push(group); + } else { + editsMap.set(item.uri, [group]); } } - } else if (edit instanceof ResourceTextEdit) { - // - const array = editsMap.get(edit.resource); - if (array) { - array.push([edit.textEdit]); - } else { - editsMap.set(edit.resource, [[edit.textEdit]]); - } } } - for (const [uri, edits] of editsMap) { const flatEdits = edits.flat(); @@ -567,9 +565,9 @@ export class HunkData { this._textModel0.pushEditOperations(null, edits, () => null); } - async recompute(editState: IChatTextEditGroupState) { + async recompute(editState: IChatTextEditGroupState, diff?: IDocumentDiff | null) { - const diff = await this._editorWorkerService.computeDiff(this._textModel0.uri, this._textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false }, 'advanced'); + diff ??= await this._editorWorkerService.computeDiff(this._textModel0.uri, this._textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false }, 'advanced'); if (!diff || diff.changes.length === 0) { // return new HunkData([], session); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index 638f0858903..6ab4ea5dbec 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; -import { EditMode, IInlineChatSession, IInlineChatResponse } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { EditMode } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IRange } from 'vs/editor/common/core/range'; import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -12,12 +12,13 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Session, StashedSession } from './inlineChatSession'; import { IValidEditOperation } from 'vs/editor/common/model'; +import { IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; export type Recording = { when: Date; - session: IInlineChatSession; - exchanges: { prompt: string; res: IInlineChatResponse }[]; + session: string; + exchanges: { prompt: string; res: IChatResponseModel }[]; }; export interface ISessionKeyComputer { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 5a890022659..e77461a562c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -22,7 +22,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { CTX_INLINE_CHAT_HAS_AGENT, EditMode, IInlineChatResponse, IInlineChatSession } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_HAS_AGENT, EditMode } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { EmptyResponse, ErrorResponse, HunkData, ReplyResponse, Session, SessionExchange, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession'; @@ -145,13 +145,6 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const textModel = editor.getModel(); const selection = editor.getSelection(); - const rawSession: IInlineChatSession = { - id: Math.random(), - wholeRange: new Range(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn), - placeholder: agent.description, - slashCommands: agent.slashCommands - }; - const store = new DisposableStore(); this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${agent.extensionId}`); @@ -172,8 +165,6 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { return; } - const modelAltVersionIdNow = textModel.getAlternativeVersionId(); - const { response } = e.request; lastResponseListener.value = response.onDidChange(() => { @@ -197,29 +188,9 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { // epmty response inlineResponse = new EmptyResponse(); } else { - // replay response - const raw: IInlineChatResponse = { - edits: { edits: [] }, - }; - for (const item of response.response.value) { - if (item.kind === 'textEditGroup') { - for (const group of item.edits) { - for (const edit of group) { - raw.edits.edits.push({ - resource: item.uri, - textEdit: edit, - versionId: undefined - }); - } - } - } - } - inlineResponse = this._instaService.createInstance( ReplyResponse, - raw, session.textModelN.uri, - modelAltVersionIdNow, e.request, response ); @@ -267,7 +238,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { let wholeRange = options.wholeRange; if (!wholeRange) { - wholeRange = rawSession.wholeRange ? Range.lift(rawSession.wholeRange) : editor.getSelection(); + wholeRange = new Range(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn); } if (token.isCancellationRequested) { @@ -281,7 +252,6 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { textModel0, textModelN, agent, - rawSession, store.add(new SessionWholeRange(textModelN, wholeRange)), store.add(new HunkData(this._editorWorkerService, textModel0, textModelN)), chatModel @@ -413,18 +383,21 @@ export class InlineChatEnabler { private readonly _ctxHasProvider: IContextKey; + private readonly _store = new DisposableStore(); + constructor( @IContextKeyService contextKeyService: IContextKeyService, @IChatAgentService chatAgentService: IChatAgentService ) { this._ctxHasProvider = CTX_INLINE_CHAT_HAS_AGENT.bindTo(contextKeyService); - chatAgentService.onDidChangeAgents(() => { + this._store.add(chatAgentService.onDidChangeAgents(() => { const hasEditorAgent = Boolean(chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)); this._ctxHasProvider.set(hasEditorAgent); - }); + })); } dispose() { this._ctxHasProvider.reset(); + this._store.dispose(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index c98fcfa30f8..2e2f692cc88 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -26,7 +26,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { Progress } from 'vs/platform/progress/common/progress'; import { SaveReason } from 'vs/workbench/common/editor'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; -import { HunkInformation, ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { HunkState } from './inlineChatSession'; @@ -136,7 +136,7 @@ export abstract class EditModeStrategy { abstract makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise; - abstract renderChanges(response: ReplyResponse): Promise; + abstract renderChanges(): Promise; move?(next: boolean): void; @@ -190,7 +190,7 @@ export class PreviewStrategy extends EditModeStrategy { override async makeProgressiveChanges(): Promise { } - override async renderChanges(response: ReplyResponse): Promise { } + override async renderChanges(): Promise { } hasFocus(): boolean { return this._zone.widget.hasFocus(); @@ -364,7 +364,7 @@ export class LiveStrategy extends EditModeStrategy { private readonly _hunkDisplayData = new Map(); - override async renderChanges(response: ReplyResponse) { + override async renderChanges() { this._progressiveEditingDecorations.clear(); @@ -531,7 +531,6 @@ export class LiveStrategy extends EditModeStrategy { if (widgetData) { this._zone.updatePositionAndHeight(widgetData.position); - this._editor.revealPositionInCenterIfOutsideViewport(widgetData.position); const remainingHunks = this._session.hunkData.pending; this._updateSummaryMessage(remainingHunks, this._session.hunkData.size); @@ -578,7 +577,7 @@ export class LiveStrategy extends EditModeStrategy { message = localize('change.0', "Nothing changed."); } else if (remaining === 1) { message = needsReview - ? localize('review.1', "$(info) Accept or discard 1 change") + ? localize('review.1', "$(info) Accept or Discard change") : localize('change.1', "1 change"); } else { message = needsReview diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index f7149145266..fad3466eefe 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -25,19 +25,19 @@ import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; -import { MenuId } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { asCssVariable, asCssVariableName, editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; +import { asCssVariable, asCssVariableName, editorBackground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { ChatModel, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, inlineChatBackground } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, inlineChatBackground, InlineChatConfigKeys, inlineChatForeground } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { chatRequestBackground } from 'vs/workbench/contrib/chat/common/chatColors'; import { Selection } from 'vs/editor/common/core/selection'; @@ -47,7 +47,8 @@ import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatWidgetViewOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { TextOnlyMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; export interface InlineChatWidgetViewState { @@ -57,30 +58,16 @@ export interface InlineChatWidgetViewState { } export interface IInlineChatWidgetConstructionOptions { - /** - * The telemetry source for all commands of this widget - */ - telemetrySource: string; - /** - * The menu that is inside the input editor, use for send, dictation - */ - inputMenuId: MenuId; - /** - * The menu that next to the input editor, use for close, config etc - */ - widgetMenuId: MenuId; + /** * The menu that rendered as button bar, use for accept, discard etc */ statusMenuId: MenuId | { menu: MenuId; options: IWorkbenchButtonBarOptions }; + /** - * The men that rendered in the lower right corner, use for feedback + * The options for the chat widget */ - feedbackMenuId?: MenuId; - - editorOverflowWidgetsDomNode?: HTMLElement; - - rendererOptions?: IChatListItemRendererOptions; + chatWidgetViewOptions?: IChatWidgetViewOptions; } export interface IInlineChatMessage { @@ -101,14 +88,13 @@ export class InlineChatWidget { [ h('div.chat-widget@chatWidget'), h('div.progress@progress'), - h('div.followUps.hidden@followUps'), h('div.previewDiff.hidden@previewDiff'), h('div.accessibleViewer@accessibleViewer'), h('div.status@status', [ h('div.label.info.hidden@infoLabel'), - h('div.actions.hidden@statusToolbar'), + h('div.actions.text-style.hidden@toolbar1'), + h('div.actions.button-style.hidden@toolbar2'), h('div.label.status.hidden@statusLabel'), - h('div.actions.hidden@feedbackToolbar'), ]), ] ); @@ -149,8 +135,6 @@ export class InlineChatWidget { this._progressBar = new ProgressBar(this._elements.progress); this._store.add(this._progressBar); - let allowRequests = false; - this.scopedContextKeyService = this._store.add(_contextKeyService.createScoped(this._elements.chatWidget)); const scopedInstaService = _instantiationService.createChild( new ServiceCollection([ @@ -166,29 +150,15 @@ export class InlineChatWidget { { resource: true }, { defaultElementHeight: 32, - renderStyle: 'compact', - renderInputOnTop: true, + renderStyle: 'minimal', + renderInputOnTop: false, renderFollowups: true, supportsFileReferences: true, - editorOverflowWidgetsDomNode: options.editorOverflowWidgetsDomNode, - rendererOptions: options.rendererOptions, - menus: { - executeToolbar: options.inputMenuId, - inputSideToolbar: options.widgetMenuId, - telemetrySource: options.telemetrySource - }, - filter: item => { - if (isWelcomeVM(item)) { - return false; - } - if (isRequestVM(item)) { - return allowRequests; - } - return true; - }, + filter: item => !isWelcomeVM(item), + ...options.chatWidgetViewOptions }, { - listForeground: editorForeground, + listForeground: inlineChatForeground, listBackground: inlineChatBackground, inputEditorBackground: inputBackground, resultEditorBackground: editorBackground @@ -199,34 +169,6 @@ export class InlineChatWidget { this._chatWidget.setVisible(true); this._store.add(this._chatWidget); - const viewModelListener = this._store.add(new MutableDisposable()); - this._store.add(this._chatWidget.onDidChangeViewModel(() => { - const model = this._chatWidget.viewModel; - - if (!model) { - allowRequests = false; - viewModelListener.clear(); - return; - } - - const updateAllowRequestsFilter = () => { - let requestCount = 0; - for (const item of model.getItems()) { - if (isRequestVM(item)) { - if (++requestCount >= 2) { - break; - } - } - } - const newAllowRequest = requestCount >= 2; - if (newAllowRequest !== allowRequests) { - allowRequests = newAllowRequest; - this._chatWidget.refilter(); - } - }; - viewModelListener.value = model.onDidChange(updateAllowRequestsFilter); - })); - const viewModelStore = this._store.add(new DisposableStore()); this._store.add(this._chatWidget.onDidChangeViewModel(() => { viewModelStore.clear(); @@ -252,26 +194,35 @@ export class InlineChatWidget { this._store.add(this._chatWidget.inputEditor.onDidBlurEditorWidget(() => this._ctxInputEditorFocused.set(false))); const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu; - const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options; - const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, statusMenuId, statusMenuOptions); + // TEXT-ONLY bar + const statusToolbarMenu = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar1, statusMenuId, { + hiddenItemStrategy: HiddenItemStrategy.NoHide, + telemetrySource: options.chatWidgetViewOptions?.menus?.telemetrySource, + actionViewItemProvider: action => action instanceof MenuItemAction ? this._instantiationService.createInstance(TextOnlyMenuEntryActionViewItem, action, { conversational: true }) : undefined, + toolbarOptions: { primaryGroup: '0_main' }, + menuOptions: { renderShortTitle: true }, + label: true, + icon: false + }); + this._store.add(statusToolbarMenu.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); + this._store.add(statusToolbarMenu); + + // BUTTON bar + const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options; + const statusButtonBar = scopedInstaService.createInstance(MenuWorkbenchButtonBar, this._elements.toolbar2, statusMenuId, { + toolbarOptions: { primaryGroup: '0_main' }, + telemetrySource: options.chatWidgetViewOptions?.menus?.telemetrySource, + menuOptions: { renderShortTitle: true }, + ...statusMenuOptions, + }); this._store.add(statusButtonBar.onDidChange(() => this._onDidChangeHeight.fire())); this._store.add(statusButtonBar); + const toggleToolbar = () => this._elements.status.classList.toggle('text', this._configurationService.getValue(InlineChatConfigKeys.ExpTextButtons)); + this._store.add(this._configurationService.onDidChangeConfiguration(e => e.affectsConfiguration(InlineChatConfigKeys.ExpTextButtons) && toggleToolbar())); + toggleToolbar(); - const workbenchToolbarOptions = { - hiddenItemStrategy: HiddenItemStrategy.NoHide, - toolbarOptions: { - primaryGroup: () => true, - useSeparatorsInPrimaryActions: true - } - }; - - if (options.feedbackMenuId) { - const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); - this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); - this._store.add(feedbackToolbar); - } this._store.add(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { @@ -280,12 +231,11 @@ export class InlineChatWidget { })); this._elements.root.tabIndex = 0; - this._elements.followUps.tabIndex = 0; this._elements.statusLabel.tabIndex = 0; this._updateAriaLabel(); // this._elements.status - this._store.add(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => { + this._store.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => { return this._elements.statusLabel.dataset['title']; })); @@ -347,7 +297,6 @@ export class InlineChatWidget { protected _doLayout(dimension: Dimension): void { const extraHeight = this._getExtraHeight(); const progressHeight = getTotalHeight(this._elements.progress); - const followUpsHeight = getTotalHeight(this._elements.followUps); const statusHeight = getTotalHeight(this._elements.status); // console.log('ZONE#Widget#layout', { height: dimension.height, extraHeight, progressHeight, followUpsHeight, statusHeight, LIST: dimension.height - progressHeight - followUpsHeight - statusHeight - extraHeight }); @@ -357,7 +306,7 @@ export class InlineChatWidget { this._elements.progress.style.width = `${dimension.width}px`; this._chatWidget.layout( - dimension.height - progressHeight - followUpsHeight - statusHeight - extraHeight, + dimension.height - progressHeight - statusHeight - extraHeight, dimension.width ); } @@ -367,13 +316,12 @@ export class InlineChatWidget { */ get contentHeight(): number { const data = { - followUpsHeight: getTotalHeight(this._elements.followUps), chatWidgetContentHeight: this._chatWidget.contentHeight, progressHeight: getTotalHeight(this._elements.progress), statusHeight: getTotalHeight(this._elements.status), extraHeight: this._getExtraHeight() }; - const result = data.progressHeight + data.chatWidgetContentHeight + data.followUpsHeight + data.statusHeight + data.extraHeight; + const result = data.progressHeight + data.chatWidgetContentHeight + data.statusHeight + data.extraHeight; return result; } @@ -396,7 +344,7 @@ export class InlineChatWidget { } protected _getExtraHeight(): number { - return 12 /* padding */ + 2 /*border*/ + 12 /*shadow*/; + return 4 /* padding */ + 2 /*border*/ + 4 /*shadow*/; } updateProgress(show: boolean) { @@ -436,8 +384,9 @@ export class InlineChatWidget { } updateToolbar(show: boolean) { - this._elements.statusToolbar.classList.toggle('hidden', !show); - this._elements.feedbackToolbar.classList.toggle('hidden', !show); + this._elements.root.classList.toggle('toolbar', show); + this._elements.toolbar1.classList.toggle('hidden', !show); + this._elements.toolbar2.classList.toggle('hidden', !show); this._elements.status.classList.toggle('actions', show); this._elements.infoLabel.classList.toggle('hidden', show); this._onDidChangeHeight.fire(); @@ -448,12 +397,12 @@ export class InlineChatWidget { if (!viewModel) { return undefined; } - for (const item of viewModel.getItems()) { - if (isResponseVM(item)) { - return viewModel.codeBlockModelCollection.get(viewModel.sessionId, item, codeBlockIndex)?.model; - } + const items = viewModel.getItems().filter(i => isResponseVM(i)); + if (!items.length) { + return; } - return undefined; + const item = items[items.length - 1]; + return viewModel.codeBlockModelCollection.get(viewModel.sessionId, item, codeBlockIndex)?.model; } get responseContent(): string | undefined { @@ -464,9 +413,6 @@ export class InlineChatWidget { return tail(requests)?.response?.response.asString(); } - get usesDefaultChatModel(): boolean { - return this.getChatModel() === this._defaultChatModel; - } getChatModel(): IChatModel { return this._chatWidget.viewModel?.model ?? this._defaultChatModel; @@ -482,7 +428,7 @@ export class InlineChatWidget { */ addToHistory(input: string) { if (this._chatWidget.viewModel?.model === this._defaultChatModel) { - this._chatWidget.input.acceptInput(input); + this._chatWidget.input.acceptInput(true); } } @@ -570,10 +516,12 @@ export class InlineChatWidget { reset(this._elements.statusLabel); this._elements.statusLabel.classList.toggle('hidden', true); - this._elements.statusToolbar.classList.add('hidden'); - this._elements.feedbackToolbar.classList.add('hidden'); + this._elements.toolbar1.classList.add('hidden'); + this._elements.toolbar2.classList.add('hidden'); this.updateInfo(''); + this.chatWidget.setModel(this._defaultChatModel, {}); + this._elements.accessibleViewer.classList.toggle('hidden', true); this._onDidChangeHeight.fire(); } @@ -608,7 +556,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { @IChatService chatService: IChatService, @IHoverService hoverService: IHoverService, ) { - super(location, { ...options, editorOverflowWidgetsDomNode: _parentEditor.getOverflowWidgetsDomNode() }, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService, hoverService); + super(location, { ...options, chatWidgetViewOptions: { ...options.chatWidgetViewOptions, editorOverflowWidgetsDomNode: _parentEditor.getOverflowWidgetsDomNode() } }, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService, hoverService); } // --- layout diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 88c16b8070d..793600ccf0c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -9,19 +9,19 @@ import { assertType } from 'vs/base/common/types'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { Range } from 'vs/editor/common/core/range'; import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, EditMode, InlineChatConfigKeys, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, EditMode, InlineChatConfigKeys, MENU_INLINE_CHAT_EXECUTE, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { EditorBasedInlineChatWidget } from './inlineChatWidget'; -import { MenuId } from 'vs/platform/actions/common/actions'; import { isEqual } from 'vs/base/common/resources'; import { StableEditorBottomScrollState } from 'vs/editor/browser/stableEditorScroll'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ILogService } from 'vs/platform/log/common/log'; export class InlineChatZoneWidget extends ZoneWidget { @@ -29,12 +29,12 @@ export class InlineChatZoneWidget extends ZoneWidget { private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; private _dimension?: Dimension; - private _indentationWidth: number | undefined; constructor( location: ChatAgentLocation, editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, + @ILogService private _logService: ILogService, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, ) { @@ -47,9 +47,6 @@ export class InlineChatZoneWidget extends ZoneWidget { })); this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, { - telemetrySource: 'interactiveEditorWidget-toolbar', - inputMenuId: MenuId.ChatExecute, - widgetMenuId: MENU_INLINE_CHAT_WIDGET, statusMenuId: { menu: MENU_INLINE_CHAT_WIDGET_STATUS, options: { @@ -64,22 +61,41 @@ export class InlineChatZoneWidget extends ZoneWidget { } } }, - rendererOptions: { - renderTextEditsAsSummary: (uri) => { - // render edits as summary only when using Live mode and when - // dealing with the current file in the editor - return isEqual(uri, editor.getModel()?.uri) - && configurationService.getValue(InlineChatConfigKeys.Mode) === EditMode.Live; + chatWidgetViewOptions: { + menus: { + executeToolbar: MENU_INLINE_CHAT_EXECUTE, + telemetrySource: 'interactiveEditorWidget-toolbar', }, + rendererOptions: { + renderTextEditsAsSummary: (uri) => { + // render edits as summary only when using Live mode and when + // dealing with the current file in the editor + return isEqual(uri, editor.getModel()?.uri) + && configurationService.getValue(InlineChatConfigKeys.Mode) === EditMode.Live; + }, + } } }); + this._disposables.add(this.widget); + + let scrollState: StableEditorBottomScrollState | undefined; + this._disposables.add(this.widget.chatWidget.onWillMaybeChangeHeight(() => { + if (this.position) { + scrollState = StableEditorBottomScrollState.capture(this.editor); + } + })); this._disposables.add(this.widget.onDidChangeHeight(() => { if (this.position) { // only relayout when visible - this._relayout(this._computeHeight().linesValue); + scrollState ??= StableEditorBottomScrollState.capture(this.editor); + const height = this._computeHeight(); + this._relayout(height.linesValue); + scrollState.restore(this.editor); + scrollState = undefined; + this._revealTopOfZoneWidget(this.position, height); } })); - this._disposables.add(this.widget); + this.create(); this._disposables.add(addDisposableListener(this.domNode, 'click', e => { @@ -110,16 +126,14 @@ export class InlineChatZoneWidget extends ZoneWidget { container.appendChild(this.widget.domNode); } - protected override _doLayout(heightInPixel: number): void { - const width = Math.min(640, this._availableSpaceGivenIndentation(this._indentationWidth)); - this._dimension = new Dimension(width, heightInPixel); - this.widget.layout(this._dimension); - } - private _availableSpaceGivenIndentation(indentationWidth: number | undefined): number { const info = this.editor.getLayoutInfo(); - return info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth + (indentationWidth ?? 0)); + let width = info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth); + width = Math.min(640, width); + + this._dimension = new Dimension(width, heightInPixel); + this.widget.layout(this._dimension); } private _computeHeight(): { linesValue: number; pixelsValue: number } { @@ -147,86 +161,67 @@ export class InlineChatZoneWidget extends ZoneWidget { const height = this._computeHeight(); super.show(position, height.linesValue); - this._setWidgetMargins(position); this.widget.chatWidget.setVisible(true); this.widget.focus(); scrollState.restore(this.editor); - if (position.lineNumber > 1) { - this.editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(position.delta(-1)), ScrollType.Immediate); - } else { - // reveal top of zone widget - const lineTop = this.editor.getTopForLineNumber(position.lineNumber); - const zoneTop = lineTop - height.pixelsValue; - const spaceBelowLine = this.editor.getScrollHeight() - this.editor.getBottomForLineNumber(position.lineNumber); - const minTop = this.editor.getScrollTop() - spaceBelowLine; - const newTop = Math.max(zoneTop, minTop); - - if (newTop < this.editor.getScrollTop()) { - this.editor.setScrollTop(newTop, ScrollType.Immediate); - } - } + this._revealTopOfZoneWidget(position, height); } override updatePositionAndHeight(position: Position): void { - super.updatePositionAndHeight(position, this._computeHeight().linesValue); - this._setWidgetMargins(position); - } + const scrollState = StableEditorBottomScrollState.capture(this.editor); + const height = this._computeHeight(); + super.updatePositionAndHeight(position, height.linesValue); + scrollState.restore(this.editor); - protected override _getWidth(info: EditorLayoutInfo): number { - return info.width - info.minimap.minimapWidth; + this._revealTopOfZoneWidget(position, height); } - updateBackgroundColor(newPosition: Position, wholeRange: IRange) { - assertType(this.container); - const widgetLineNumber = newPosition.lineNumber; - this.container.classList.toggle('inside-selection', widgetLineNumber > wholeRange.startLineNumber && widgetLineNumber < wholeRange.endLineNumber); - } + private _revealTopOfZoneWidget(position: Position, height: { linesValue: number; pixelsValue: number }) { - private _calculateIndentationWidth(position: Position): number { - const viewModel = this.editor._getViewModel(); - if (!viewModel) { - return 0; - } + // reveal top of zone widget + + const lineNumber = position.lineNumber <= 1 ? 1 : 1 + position.lineNumber; - const visibleRange = viewModel.getCompletelyVisibleViewRange(); - if (!visibleRange.containsPosition(position)) { - // this is needed because `getOffsetForColumn` won't work when the position - // isn't visible/rendered - return 0; + const scrollTop = this.editor.getScrollTop(); + const lineTop = this.editor.getTopForLineNumber(lineNumber); + const zoneTop = lineTop - height.pixelsValue; + + const editorHeight = this.editor.getLayoutInfo().height; + const lineBottom = this.editor.getBottomForLineNumber(lineNumber); + + let newScrollTop = zoneTop; + let forceScrollTop = false; + + if (lineBottom >= (scrollTop + editorHeight)) { + // revealing the top of the zone would pust out the line we are interested it and + // therefore we keep the line in the view port + newScrollTop = lineBottom - editorHeight; + forceScrollTop = true; } - let indentationLevel = viewModel.getLineFirstNonWhitespaceColumn(position.lineNumber); - let indentationLineNumber = position.lineNumber; - for (let lineNumber = position.lineNumber; lineNumber >= visibleRange.startLineNumber; lineNumber--) { - const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber); - if (currentIndentationLevel !== 0) { - indentationLineNumber = lineNumber; - indentationLevel = currentIndentationLevel; - break; - } + if (newScrollTop < scrollTop || forceScrollTop) { + this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop }); + this.editor.setScrollTop(newScrollTop, ScrollType.Immediate); } + } - return Math.max(0, this.editor.getOffsetForColumn(indentationLineNumber, indentationLevel)); // double-guard against invalie getOffsetForColumn-calls + protected override revealRange(range: Range, isLastLine: boolean): void { + // noop } - private _setWidgetMargins(position: Position): void { - const indentationWidth = this._calculateIndentationWidth(position); - if (this._indentationWidth === indentationWidth) { - return; - } - this._indentationWidth = this._availableSpaceGivenIndentation(indentationWidth) > 400 ? indentationWidth : 0; - this.widget.domNode.style.marginLeft = `${this._indentationWidth}px`; - this.widget.domNode.style.marginRight = `${this.editor.getLayoutInfo().minimap.minimapWidth}px`; + protected override _getWidth(info: EditorLayoutInfo): number { + return info.width - info.minimap.minimapWidth; } override hide(): void { - this.container!.classList.remove('inside-selection'); + const scrollState = StableEditorBottomScrollState.capture(this.editor); this._ctxCursorPosition.reset(); this.widget.reset(); this.widget.chatWidget.setVisible(false); super.hide(); aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); + scrollState.restore(this.editor); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index a2d1666ef79..279fea6e71b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -11,20 +11,18 @@ max-width: unset; } -.monaco-workbench .zone-widget-container.inside-selection { - background-color: var(--vscode-inlineChat-regionHighlight); -} - .monaco-workbench .inline-chat { color: inherit; - padding: 0 8px 8px 8px; border-radius: 4px; border: 1px solid var(--vscode-inlineChat-border); box-shadow: 0 2px 4px 0 var(--vscode-widget-shadow); - margin-top: 8px; background: var(--vscode-inlineChat-background); } +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part { + padding: 4px 6px 0 6px; +} + .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-execute-toolbar { margin-bottom: 1px; } @@ -39,18 +37,31 @@ } .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-item-container.interactive-item-compact { - padding: 6px 4px; gap: 6px; + padding-top: 2px; + padding-right: 20px; + padding-left: 6px; } .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-item-container.interactive-item-compact .header .avatar { outline-offset: -1px; } +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-item-container.interactive-item-compact .chat-notification-widget { + margin-bottom: 0; + padding: 0; + border: none; +} + .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-request { border: none; } +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-item-container.minimal > .header { + top: 5px; + right: 10px; +} + /* progress bit */ .monaco-workbench .inline-chat .progress { @@ -64,11 +75,11 @@ /* status */ -.monaco-workbench .inline-chat .status { +.monaco-workbench .inline-chat > .status { display: flex; justify-content: space-between; align-items: center; - margin-top: 3px; /*makes space for action focus borders: https://github.com/microsoft/vscode-copilot/issues/5814 */ + padding: 4px 6px 0 6px } .monaco-workbench .inline-chat .status .actions.hidden { @@ -78,8 +89,9 @@ .monaco-workbench .inline-chat .status .label { overflow: hidden; color: var(--vscode-descriptionForeground); - font-size: 12px; - display: inline-flex; + font-size: 11px; + display: flex; + white-space: nowrap; } .monaco-workbench .inline-chat .status .label.info { @@ -104,7 +116,7 @@ } .monaco-workbench .inline-chat .status .label > .codicon { - padding: 0 5px; + padding: 0 3px; font-size: 12px; line-height: 18px; } @@ -141,14 +153,61 @@ display: none; } -.monaco-workbench .inline-chat .status .actions { +.monaco-workbench .inline-chat .status .actions, +.monaco-workbench .inline-chat-content-widget .toolbar { + display: flex; - padding-top: 3px; + height: 18px; + + .actions-container { + gap: 3px + } + + .action-item.text-only .action-label { + font-size: 12px; + line-height: 16px; + padding: 0 4px; + border-radius: 2px; + } + + .monaco-action-bar .action-item.menu-entry.text-only + .action-item:not(.text-only) > .monaco-dropdown .action-label { + font-size: 12px; + line-height: 16px; + width: unset; + height: unset; + } +} + +.monaco-workbench .inline-chat .status .actions, +.monaco-workbench .inline-chat-content-widget.contents .toolbar { + + .monaco-action-bar .action-item.menu-entry.text-only:first-of-type .action-label{ + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-background); + } +} + +.monaco-workbench .inline-chat .status { + .actions.text-style { + display: none; + } + .actions.button-style { + display: inherit; + } +} + +.monaco-workbench .inline-chat .status.text { + .actions.text-style { + display: inherit; + } + .actions.button-style { + display: none; + } } .monaco-workbench .inline-chat .status .actions > .monaco-button, .monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown { - margin-right: 6px; + margin-right: 4px; } .monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown > .monaco-dropdown-button { @@ -166,12 +225,8 @@ } .monaco-workbench .inline-chat .status .actions .monaco-text-button { - padding: 2px 4px; - white-space: nowrap; -} - -.monaco-workbench .inline-chat .status .monaco-toolbar .action-item { padding: 0 2px; + white-space: nowrap; } /* TODO@jrieken not needed? */ diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css index bc9e18e6ea6..7da5cc3e97e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css @@ -5,15 +5,12 @@ .monaco-workbench .inline-chat-content-widget { z-index: 50; - padding: 6px 6px 6px 6px; + padding: 6px; border-radius: 4px; background-color: var(--vscode-inlineChat-background); box-shadow: 0 4px 8px var(--vscode-inlineChat-shadow); } -.monaco-workbench .inline-chat-content-widget .hidden { - display: none; -} .monaco-workbench .inline-chat-content-widget.interactive-session .interactive-session { max-width: unset; @@ -27,15 +24,11 @@ padding: 0; } -.monaco-workbench .inline-chat-content-widget .message { - overflow: hidden; - color: var(--vscode-descriptionForeground); - font-size: 11px; - display: inline-flex; +.monaco-workbench .inline-chat-content-widget.interactive-session .interactive-list { + display: none; } -.monaco-workbench .inline-chat-content-widget .message > .codicon { - padding-right: 5px; - font-size: 12px; - line-height: 18px; +.monaco-workbench .inline-chat-content-widget.interactive-session .toolbar { + display: none; + padding-top: 4px; } diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 00954ac4bfa..b4c754b1759 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -3,118 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { IRange } from 'vs/editor/common/core/range'; -import { WorkspaceEdit } from 'vs/editor/common/languages'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; -import { diffInserted, diffRemoved, editorHoverHighlight, editorWidgetBackground, editorWidgetBorder, focusBorder, inputBackground, inputPlaceholderForeground, registerColor, transparent, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; -import { Extensions as ExtensionsMigration, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; -import { IChatAgentCommand } from 'vs/workbench/contrib/chat/common/chatAgents'; - -export interface IInlineChatSession { - id: number; - placeholder?: string; - input?: string; - message?: string; - slashCommands?: IChatAgentCommand[]; - wholeRange?: IRange; -} - -export const enum InlineChatResponseTypes { - Empty = 'empty', - OnlyEdits = 'onlyEdits', - OnlyMessages = 'onlyMessages', - Mixed = 'mixed' -} - -export interface IInlineChatResponse { - edits: WorkspaceEdit; - message?: IMarkdownString; - placeholder?: string; - wholeRange?: IRange; -} - - -export const INLINE_CHAT_ID = 'interactiveEditor'; -export const INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID = 'interactiveEditorAccessiblityHelp'; - -export const enum EditMode { - Live = 'live', - Preview = 'preview' -} - -export const CTX_INLINE_CHAT_HAS_AGENT = new RawContextKey('inlineChatHasProvider', false, localize('inlineChatHasProvider', "Whether a provider for interactive editors exists")); -export const CTX_INLINE_CHAT_VISIBLE = new RawContextKey('inlineChatVisible', false, localize('inlineChatVisible', "Whether the interactive editor input is visible")); -export const CTX_INLINE_CHAT_FOCUSED = new RawContextKey('inlineChatFocused', false, localize('inlineChatFocused', "Whether the interactive editor input is focused")); -export const CTX_INLINE_CHAT_RESPONSE_FOCUSED = new RawContextKey('inlineChatResponseFocused', false, localize('inlineChatResponseFocused', "Whether the interactive widget's response is focused")); -export const CTX_INLINE_CHAT_EMPTY = new RawContextKey('inlineChatEmpty', false, localize('inlineChatEmpty', "Whether the interactive editor input is empty")); -export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('inlineChatInnerCursorFirst', false, localize('inlineChatInnerCursorFirst', "Whether the cursor of the iteractive editor input is on the first line")); -export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); -export const CTX_INLINE_CHAT_INNER_CURSOR_START = new RawContextKey('inlineChatInnerCursorStart', false, localize('inlineChatInnerCursorStart', "Whether the cursor of the iteractive editor input is on the start of the input")); -export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey('inlineChatInnerCursorEnd', false, localize('inlineChatInnerCursorEnd', "Whether the cursor of the iteractive editor input is on the end of the input")); -export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); -export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey('inlineChatHasStashedSession', false, localize('inlineChatHasStashedSession', "Whether interactive editor has kept a session for quick restore")); -export const CTX_INLINE_CHAT_RESPONSE_TYPES = new RawContextKey('inlineChatResponseTypes', InlineChatResponseTypes.Empty, localize('inlineChatResponseTypes', "What type was the responses have been receieved")); -export const CTX_INLINE_CHAT_USER_DID_EDIT = new RawContextKey('inlineChatUserDidEdit', undefined, localize('inlineChatUserDidEdit', "Whether the user did changes ontop of the inline chat")); -export const CTX_INLINE_CHAT_DOCUMENT_CHANGED = new RawContextKey('inlineChatDocumentChanged', false, localize('inlineChatDocumentChanged', "Whether the document has changed concurrently")); -export const CTX_INLINE_CHAT_CHANGE_HAS_DIFF = new RawContextKey('inlineChatChangeHasDiff', false, localize('inlineChatChangeHasDiff', "Whether the current change supports showing a diff")); -export const CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF = new RawContextKey('inlineChatChangeShowsDiff', false, localize('inlineChatChangeShowsDiff', "Whether the current change showing a diff")); -export const CTX_INLINE_CHAT_EDIT_MODE = new RawContextKey('config.inlineChat.mode', EditMode.Live); - -// --- (select) action identifier - -export const ACTION_ACCEPT_CHANGES = 'inlineChat.acceptChanges'; -export const ACTION_REGENERATE_RESPONSE = 'inlineChat.regenerate'; -export const ACTION_VIEW_IN_CHAT = 'inlineChat.viewInChat'; -export const ACTION_TOGGLE_DIFF = 'inlineChat.toggleDiff'; - -// --- menus - -export const MENU_INLINE_CHAT_WIDGET = MenuId.for('inlineChatWidget'); -export const MENU_INLINE_CHAT_WIDGET_STATUS = MenuId.for('inlineChatWidget.status'); - -// --- colors - - -export const inlineChatBackground = registerColor('inlineChat.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, localize('inlineChat.background', "Background color of the interactive editor widget")); -export const inlineChatBorder = registerColor('inlineChat.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, localize('inlineChat.border', "Border color of the interactive editor widget")); -export const inlineChatShadow = registerColor('inlineChat.shadow', { dark: widgetShadow, light: widgetShadow, hcDark: widgetShadow, hcLight: widgetShadow }, localize('inlineChat.shadow', "Shadow color of the interactive editor widget")); -export const inlineChatRegionHighlight = registerColor('inlineChat.regionHighlight', { dark: editorHoverHighlight, light: editorHoverHighlight, hcDark: editorHoverHighlight, hcLight: editorHoverHighlight }, localize('inlineChat.regionHighlight', "Background highlighting of the current interactive region. Must be transparent."), true); -export const inlineChatInputBorder = registerColor('inlineChatInput.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, localize('inlineChatInput.border', "Border color of the interactive editor input")); -export const inlineChatInputFocusBorder = registerColor('inlineChatInput.focusBorder', { dark: focusBorder, light: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, localize('inlineChatInput.focusBorder', "Border color of the interactive editor input when focused")); -export const inlineChatInputPlaceholderForeground = registerColor('inlineChatInput.placeholderForeground', { dark: inputPlaceholderForeground, light: inputPlaceholderForeground, hcDark: inputPlaceholderForeground, hcLight: inputPlaceholderForeground }, localize('inlineChatInput.placeholderForeground', "Foreground color of the interactive editor input placeholder")); -export const inlineChatInputBackground = registerColor('inlineChatInput.background', { dark: inputBackground, light: inputBackground, hcDark: inputBackground, hcLight: inputBackground }, localize('inlineChatInput.background', "Background color of the interactive editor input")); - -export const inlineChatDiffInserted = registerColor('inlineChatDiff.inserted', { dark: transparent(diffInserted, .5), light: transparent(diffInserted, .5), hcDark: transparent(diffInserted, .5), hcLight: transparent(diffInserted, .5) }, localize('inlineChatDiff.inserted', "Background color of inserted text in the interactive editor input")); -export const overviewRulerInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); -export const minimapInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); - -export const inlineChatDiffRemoved = registerColor('inlineChatDiff.removed', { dark: transparent(diffRemoved, .5), light: transparent(diffRemoved, .5), hcDark: transparent(diffRemoved, .5), hcLight: transparent(diffRemoved, .5) }, localize('inlineChatDiff.removed', "Background color of removed text in the interactive editor input")); -export const overviewRulerInlineChatDiffRemoved = registerColor('editorOverviewRuler.inlineChatRemoved', { dark: transparent(diffRemoved, 0.6), light: transparent(diffRemoved, 0.8), hcDark: transparent(diffRemoved, 0.6), hcLight: transparent(diffRemoved, 0.8) }, localize('editorOverviewRuler.inlineChatRemoved', 'Overview ruler marker color for inline chat removed content.')); - +import { diffInserted, diffRemoved, editorWidgetBackground, editorWidgetBorder, editorWidgetForeground, focusBorder, inputBackground, inputPlaceholderForeground, registerColor, transparent, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; // settings - - -Registry.as(ExtensionsMigration.ConfigurationMigration).registerConfigurationMigrations( - [{ - key: 'interactiveEditor.editMode', migrateFn: (value: any) => { - return [['inlineChat.mode', { value: value }]]; - } - }] -); - export const enum InlineChatConfigKeys { Mode = 'inlineChat.mode', FinishOnType = 'inlineChat.finishOnType', AcceptedOrDiscardBeforeSave = 'inlineChat.acceptedOrDiscardBeforeSave', HoldToSpeech = 'inlineChat.holdToSpeech', - AccessibleDiffView = 'inlineChat.accessibleDiffView' + AccessibleDiffView = 'inlineChat.accessibleDiffView', + ExpTextButtons = 'inlineChat.experimental.textButtons' +} + +export const enum EditMode { + Live = 'live', + Preview = 'preview' } Registry.as(Extensions.Configuration).registerConfiguration({ @@ -156,6 +65,77 @@ Registry.as(Extensions.Configuration).registerConfigurat localize('accessibleDiffView.on', "The accessible diff viewer is always enabled."), localize('accessibleDiffView.off', "The accessible diff viewer is never enabled."), ], - } + }, + [InlineChatConfigKeys.ExpTextButtons]: { + description: localize('txtButtons', "Whether to use textual buttons."), + default: false, + type: 'boolean', + tags: ['experimental'] + }, } }); + + +export const INLINE_CHAT_ID = 'interactiveEditor'; +export const INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID = 'interactiveEditorAccessiblityHelp'; + +// --- CONTEXT + +export const enum InlineChatResponseType { + None = 'none', + Messages = 'messages', + MessagesAndEdits = 'messagesAndEdits' +} + +export const CTX_INLINE_CHAT_HAS_AGENT = new RawContextKey('inlineChatHasProvider', false, localize('inlineChatHasProvider', "Whether a provider for interactive editors exists")); +export const CTX_INLINE_CHAT_VISIBLE = new RawContextKey('inlineChatVisible', false, localize('inlineChatVisible', "Whether the interactive editor input is visible")); +export const CTX_INLINE_CHAT_FOCUSED = new RawContextKey('inlineChatFocused', false, localize('inlineChatFocused', "Whether the interactive editor input is focused")); +export const CTX_INLINE_CHAT_RESPONSE_FOCUSED = new RawContextKey('inlineChatResponseFocused', false, localize('inlineChatResponseFocused', "Whether the interactive widget's response is focused")); +export const CTX_INLINE_CHAT_EMPTY = new RawContextKey('inlineChatEmpty', false, localize('inlineChatEmpty', "Whether the interactive editor input is empty")); +export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('inlineChatInnerCursorFirst', false, localize('inlineChatInnerCursorFirst', "Whether the cursor of the iteractive editor input is on the first line")); +export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); +export const CTX_INLINE_CHAT_INNER_CURSOR_START = new RawContextKey('inlineChatInnerCursorStart', false, localize('inlineChatInnerCursorStart', "Whether the cursor of the iteractive editor input is on the start of the input")); +export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey('inlineChatInnerCursorEnd', false, localize('inlineChatInnerCursorEnd', "Whether the cursor of the iteractive editor input is on the end of the input")); +export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); +export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey('inlineChatHasStashedSession', false, localize('inlineChatHasStashedSession', "Whether interactive editor has kept a session for quick restore")); +export const CTX_INLINE_CHAT_USER_DID_EDIT = new RawContextKey('inlineChatUserDidEdit', undefined, localize('inlineChatUserDidEdit', "Whether the user did changes ontop of the inline chat")); +export const CTX_INLINE_CHAT_DOCUMENT_CHANGED = new RawContextKey('inlineChatDocumentChanged', false, localize('inlineChatDocumentChanged', "Whether the document has changed concurrently")); +export const CTX_INLINE_CHAT_CHANGE_HAS_DIFF = new RawContextKey('inlineChatChangeHasDiff', false, localize('inlineChatChangeHasDiff', "Whether the current change supports showing a diff")); +export const CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF = new RawContextKey('inlineChatChangeShowsDiff', false, localize('inlineChatChangeShowsDiff', "Whether the current change showing a diff")); +export const CTX_INLINE_CHAT_EDIT_MODE = new RawContextKey('config.inlineChat.mode', EditMode.Live); +export const CTX_INLINE_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('inlineChatRequestInProgress', false, localize('inlineChatRequestInProgress', "Whether an inline chat request is currently in progress")); +export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey('inlineChatResponseType', InlineChatResponseType.None, localize('inlineChatResponseTypes', "What type was the responses have been receieved, nothing yet, just messages, or messaged and local edits")); + +export const CTX_INLINE_CHAT_CONFIG_TXT_BTNS = ContextKeyExpr.equals(`config.${[InlineChatConfigKeys.ExpTextButtons]}`, true); + +// --- (selected) action identifier + +export const ACTION_ACCEPT_CHANGES = 'inlineChat.acceptChanges'; +export const ACTION_REGENERATE_RESPONSE = 'inlineChat.regenerate'; +export const ACTION_VIEW_IN_CHAT = 'inlineChat.viewInChat'; +export const ACTION_TOGGLE_DIFF = 'inlineChat.toggleDiff'; + +// --- menus + +export const MENU_INLINE_CHAT_EXECUTE = MenuId.for('inlineChat.execute'); +export const MENU_INLINE_CHAT_CONTENT_STATUS = MenuId.for('inlineChat.content.status'); +export const MENU_INLINE_CHAT_WIDGET_STATUS = MenuId.for('inlineChatWidget.status'); + +// --- colors + + +export const inlineChatForeground = registerColor('inlineChat.foreground', editorWidgetForeground, localize('inlineChat.foreground', "Foreground color of the interactive editor widget")); +export const inlineChatBackground = registerColor('inlineChat.background', editorWidgetBackground, localize('inlineChat.background', "Background color of the interactive editor widget")); +export const inlineChatBorder = registerColor('inlineChat.border', editorWidgetBorder, localize('inlineChat.border', "Border color of the interactive editor widget")); +export const inlineChatShadow = registerColor('inlineChat.shadow', widgetShadow, localize('inlineChat.shadow', "Shadow color of the interactive editor widget")); +export const inlineChatInputBorder = registerColor('inlineChatInput.border', editorWidgetBorder, localize('inlineChatInput.border', "Border color of the interactive editor input")); +export const inlineChatInputFocusBorder = registerColor('inlineChatInput.focusBorder', focusBorder, localize('inlineChatInput.focusBorder', "Border color of the interactive editor input when focused")); +export const inlineChatInputPlaceholderForeground = registerColor('inlineChatInput.placeholderForeground', inputPlaceholderForeground, localize('inlineChatInput.placeholderForeground', "Foreground color of the interactive editor input placeholder")); +export const inlineChatInputBackground = registerColor('inlineChatInput.background', inputBackground, localize('inlineChatInput.background', "Background color of the interactive editor input")); + +export const inlineChatDiffInserted = registerColor('inlineChatDiff.inserted', transparent(diffInserted, .5), localize('inlineChatDiff.inserted', "Background color of inserted text in the interactive editor input")); +export const overviewRulerInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); +export const minimapInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); + +export const inlineChatDiffRemoved = registerColor('inlineChatDiff.removed', transparent(diffRemoved, .5), localize('inlineChatDiff.removed', "Background color of removed text in the interactive editor input")); +export const overviewRulerInlineChatDiffRemoved = registerColor('editorOverviewRuler.inlineChatRemoved', { dark: transparent(diffRemoved, 0.6), light: transparent(diffRemoved, 0.8), hcDark: transparent(diffRemoved, 0.6), hcLight: transparent(diffRemoved, 0.8) }, localize('editorOverviewRuler.inlineChatRemoved', 'Overview ruler marker color for inline chat removed content.')); 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 536ef76809a..cc4ddad6e7a 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { equals } from 'vs/base/common/arrays'; -import { raceCancellation, timeout } from 'vs/base/common/async'; +import { DeferredPromise, raceCancellation, timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; @@ -31,7 +31,7 @@ import { IView, IViewDescriptorService } from 'vs/workbench/common/views'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { IChatAccessibilityService, IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, ChatAgentService, IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { InlineChatController, InlineChatRunOptions, State } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; @@ -42,7 +42,7 @@ import { IInlineChatSessionService } from '../../browser/inlineChatSessionServic import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl'; import { TestWorkerService } from './testWorkerService'; import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; @@ -89,21 +89,21 @@ suite('InteractiveChatController', function () { readonly states: readonly State[] = []; - waitFor(states: readonly State[]): Promise { + awaitStates(states: readonly State[]): Promise { const actual: State[] = []; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const d = this.onDidChangeState(state => { actual.push(state); if (equals(states, actual)) { d.dispose(); - resolve(); + resolve(undefined); } }); setTimeout(() => { d.dispose(); - reject(new Error(`timeout, \nEXPECTED: ${states.join('>')}, \nACTUAL : ${actual.join('>')}`)); + resolve(`[${states.join(',')}] <> [${actual.join(',')}]`); }, 1000); }); } @@ -156,6 +156,11 @@ suite('InteractiveChatController', function () { [IChatWidgetService, new SyncDescriptor(ChatWidgetService)], [IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)], [IChatService, new SyncDescriptor(ChatService)], + [IChatAgentNameService, new class extends mock() { + override getAgentNameRestriction(chatAgentData: IChatAgentData): boolean { + return false; + } + }], [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IContextKeyService, contextKeyService], [IChatAgentService, new SyncDescriptor(ChatAgentService)], @@ -243,9 +248,9 @@ suite('InteractiveChatController', function () { test('run (show/hide)', async function () { ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor(TestController.INIT_SEQUENCE_AUTO_SEND); + const actualStates = ctrl.awaitStates(TestController.INIT_SEQUENCE_AUTO_SEND); const run = ctrl.run({ message: 'Hello', autoSend: true }); - await p; + assert.strictEqual(await actualStates, undefined); assert.ok(ctrl.getWidgetPosition() !== undefined); await ctrl.cancelSession(); @@ -274,10 +279,10 @@ suite('InteractiveChatController', function () { configurationService.setUserConfiguration(InlineChatConfigKeys.FinishOnType, true); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor(TestController.INIT_SEQUENCE_AUTO_SEND); + const actualStates = ctrl.awaitStates(TestController.INIT_SEQUENCE_AUTO_SEND); const r = ctrl.run({ message: 'Hello', autoSend: true }); - await p; + assert.strictEqual(await actualStates, undefined); const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); assert.ok(session); @@ -286,7 +291,7 @@ suite('InteractiveChatController', function () { editor.setSelection(new Range(2, 1, 2, 1)); editor.trigger('test', 'type', { text: 'a' }); - await ctrl.waitFor([State.ACCEPT]); + assert.strictEqual(await ctrl.awaitStates([State.ACCEPT]), undefined); await r; }); @@ -313,17 +318,19 @@ suite('InteractiveChatController', function () { })); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor(TestController.INIT_SEQUENCE); + const p = ctrl.awaitStates(TestController.INIT_SEQUENCE); const r = ctrl.run({ message: 'GENGEN', autoSend: false }); - await p; + assert.strictEqual(await p, undefined); + const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); assert.ok(session); assert.deepStrictEqual(session.wholeRange.value, new Range(3, 1, 3, 3)); // initial + ctrl.chatWidget.setInput('GENGEN'); ctrl.acceptInput(); - await ctrl.waitFor([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + assert.strictEqual(await ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]), undefined); assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 4, 3)); @@ -343,10 +350,11 @@ suite('InteractiveChatController', function () { })); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); const r = ctrl.run({ message: 'Hello', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); + ctrl.acceptSession(); await r; @@ -372,9 +380,9 @@ suite('InteractiveChatController', function () { const valueThen = editor.getModel().getValue(); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: 'Hello', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); ctrl.acceptSession(); await r; @@ -415,9 +423,9 @@ suite('InteractiveChatController', function () { // store.add(editor.getModel().onDidChangeContent(() => { modelChangeCounter++; })); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: 'Hello', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); // assert.ok(modelChangeCounter > 0, modelChangeCounter.toString()); // some changes have been made // const modelChangeCounterNow = modelChangeCounter; @@ -438,9 +446,9 @@ suite('InteractiveChatController', function () { // NO manual edits -> cancel ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: 'GENERATED', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); assert.ok(model.getValue().includes('GENERATED')); assert.strictEqual(contextKeyService.getContextKeyValue(CTX_INLINE_CHAT_USER_DID_EDIT.key), undefined); @@ -454,9 +462,9 @@ suite('InteractiveChatController', function () { // manual edits -> finish ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: 'GENERATED', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); assert.ok(model.getValue().includes('GENERATED')); @@ -489,16 +497,17 @@ suite('InteractiveChatController', function () { model.setValue(''); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: 'PROMPT_', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); + assert.strictEqual(model.getValue(), 'PROMPT_1'); - const p2 = ctrl.waitFor([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor); - await p2; + assert.strictEqual(await p2, undefined); assert.strictEqual(model.getValue(), 'PROMPT_2'); ctrl.finishExistingSession(); @@ -529,23 +538,24 @@ suite('InteractiveChatController', function () { model.setValue(''); // REQUEST 1 - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: '1', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); assert.strictEqual(model.getValue(), 'eins-'); // REQUEST 2 - const p2 = ctrl.waitFor([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + ctrl.chatWidget.setInput('1'); await ctrl.acceptInput(); - await p2; + assert.strictEqual(await p2, undefined); assert.strictEqual(model.getValue(), 'zwei-eins-'); // REQUEST 2 - RERUN - const p3 = ctrl.waitFor([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p3 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor); - await p3; + assert.strictEqual(await p3, undefined); assert.strictEqual(model.getValue(), 'drei-eins-'); @@ -572,10 +582,9 @@ suite('InteractiveChatController', function () { ctrl = instaService.createInstance(TestController, editor); // REQUEST 1 - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); ctrl.run({ message: '1', autoSend: true }); - await p; - + assert.strictEqual(await p, undefined); assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n'); @@ -613,16 +622,17 @@ suite('InteractiveChatController', function () { ctrl = instaService.createInstance(TestController, editor); // REQUEST 1 - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); ctrl.run({ message: '1', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n'); // REQUEST 2 - const p2 = ctrl.waitFor([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + ctrl.chatWidget.setInput('1'); await ctrl.acceptInput(); - await p2; + assert.strictEqual(await p2, undefined); assert.strictEqual(model.getValue(), 'zwei\neins\nHello\nWorld\nHello Again\nHello World\n'); @@ -672,17 +682,17 @@ suite('InteractiveChatController', function () { ctrl = instaService.createInstance(TestController, editor); // REQUEST 1 - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); ctrl.run({ message: 'Hello-', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); // resend pending request without command detection const request = ctrl.chatWidget.viewModel?.model.getRequests().at(-1); assertType(request); - const p2 = ctrl.waitFor([State.SHOW_REQUEST, State.SHOW_RESPONSE]); + const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE]); chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt + 1, location: ChatAgentLocation.Editor }); - await p2; + assert.strictEqual(await p2, undefined); assert.deepStrictEqual(commandDetection, [true, false]); assert.strictEqual(model.getValue(), 'Hello-1'); @@ -708,19 +718,107 @@ suite('InteractiveChatController', function () { ctrl = instaService.createInstance(TestController, editor); // REQUEST 1 - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); ctrl.run({ message: 'Hello-', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); // resend pending request without command detection const request = ctrl.chatWidget.viewModel?.model.getRequests().at(-1); assertType(request); - const p2 = ctrl.waitFor([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt + 1, location: ChatAgentLocation.Editor }); - await p2; + assert.strictEqual(await p2, undefined); assert.deepStrictEqual(commandDetection, [true, false]); assert.strictEqual(model.getValue(), 'Hello-1'); }); + + + test('Inline: Pressing Rerun request while the response streams breaks the response #5442', async function () { + + model.setValue('two\none\n'); + + const attempts: (number | undefined)[] = []; + + const deferred = new DeferredPromise(); + + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { + + attempts.push(request.attempt); + + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: `TRY:${request.attempt}\n` }] }); + await raceCancellation(deferred.p, token); + deferred.complete(); + await timeout(10); + return {}; + }, + })); + + ctrl = instaService.createInstance(TestController, editor); + + // REQUEST 1 + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); + ctrl.run({ message: 'Hello-', autoSend: true }); + assert.strictEqual(await p, undefined); + assert.deepStrictEqual(attempts, [0]); + + // RERUN (cancel, undo, redo) + const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const rerun = new RerunAction(); + await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor); + assert.strictEqual(await p2, undefined); + + assert.deepStrictEqual(attempts, [0, 1]); + + assert.strictEqual(model.getValue(), 'TRY:1\ntwo\none\n'); + + }); + + test('Stopping/cancelling a request should undo its changes', async function () { + + model.setValue('World'); + + const deferred = new DeferredPromise(); + let progress: ((part: IChatProgress) => void) | undefined; + + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, _progress, history, token) { + + progress = _progress; + await deferred.p; + return {}; + }, + })); + + ctrl = instaService.createInstance(TestController, editor); + + // REQUEST 1 + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); + ctrl.run({ message: 'Hello', autoSend: true }); + assert.strictEqual(await p, undefined); + + assertType(progress); + + const modelChange = new Promise(resolve => model.onDidChangeContent(() => resolve())); + + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello-Hello' }] }); + + await modelChange; + assert.strictEqual(model.getValue(), 'HelloWorld'); // first word has been streamed + + const p2 = ctrl.awaitStates([State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + chatService.cancelCurrentRequestForSession(ctrl.chatWidget.viewModel!.model.sessionId); + assert.strictEqual(await p2, undefined); + + assert.strictEqual(model.getValue(), 'World'); + + }); }); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 86a3b614ff2..26e72175e2c 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { mock } from 'vs/base/test/common/mock'; diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts index e8d229d81fc..c6724b095e4 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts @@ -7,7 +7,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IntervalTimer } from 'vs/base/common/async'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { asProgressiveEdit } from '../../browser/utils'; -import * as assert from 'assert'; +import assert from 'assert'; suite('AsyncEdit', () => { diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index cc21b0371a6..2109f7ef705 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -23,6 +23,7 @@ import { Context as SuggestContext } from 'vs/editor/contrib/suggest/browser/sug import { localize, localize2 } from 'vs/nls'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -54,6 +55,9 @@ import { CellEditType, CellKind, CellUri, INTERACTIVE_WINDOW_EDITOR_ID, Notebook import { InteractiveWindowOpen } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { executeReplInput } from 'vs/workbench/contrib/replNotebook/browser/repl.contribution'; +import { ReplEditor } from 'vs/workbench/contrib/replNotebook/browser/replEditor'; +import { ReplEditorInput } from 'vs/workbench/contrib/replNotebook/browser/replEditorInput'; import { columnToEditorGroup } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; @@ -95,7 +99,7 @@ export class InteractiveDocumentContribution extends Disposable implements IWork providerDisplayName: 'Interactive Notebook', displayName: 'Interactive', filenamePattern: ['*.interactive'], - exclusive: true + priority: RegisteredEditorPriority.exclusive })); } @@ -411,10 +415,10 @@ registerAction2(class extends Action2 { logService.debug('Open new interactive window:', notebookUri.toString(), inputUri.toString()); if (id) { - const allKernels = kernelService.getMatchingKernel({ uri: notebookUri, viewType: 'interactive' }).all; + const allKernels = kernelService.getMatchingKernel({ uri: notebookUri, notebookType: 'interactive' }).all; const preferredKernel = allKernels.find(kernel => kernel.id === id); if (preferredKernel) { - kernelService.preselectKernelForNotebook(preferredKernel, { uri: notebookUri, viewType: 'interactive' }); + kernelService.preselectKernelForNotebook(preferredKernel, { uri: notebookUri, notebookType: 'interactive' }); } } @@ -428,6 +432,25 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'interactive.configure', + title: localize2('interactive.configExecute', 'Configure input box behavior'), + category: interactiveWindowCategory, + f1: false, + icon: icons.configIcon, + menu: { + id: MenuId.InteractiveInputConfig + } + }); + } + + override run(accessor: ServicesAccessor, ...args: any[]): void { + accessor.get(ICommandService).executeCommand('workbench.action.openSettings', '@tag:replExecute'); + } +}); + registerAction2(class extends Action2 { constructor() { super({ @@ -435,6 +458,11 @@ registerAction2(class extends Action2 { title: localize2('interactive.execute', 'Execute Code'), category: interactiveWindowCategory, keybinding: [{ + // when: NOTEBOOK_CELL_LIST_FOCUSED, + when: ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), + primary: KeyMod.CtrlCmd | KeyCode.Enter, + weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT + }, { when: ContextKeyExpr.and( ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', true) @@ -448,18 +476,13 @@ registerAction2(class extends Action2 { ), primary: KeyCode.Enter, weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT - }, { - // when: NOTEBOOK_CELL_LIST_FOCUSED, - when: ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), - primary: KeyMod.WinCtrl | KeyCode.Enter, - win: { - primary: KeyMod.CtrlCmd | KeyCode.Enter - }, - weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT }], menu: [ { id: MenuId.InteractiveInputExecute + }, + { + id: MenuId.ReplInputExecute } ], icon: icons.executeIcon, @@ -483,21 +506,29 @@ registerAction2(class extends Action2 { const historyService = accessor.get(IInteractiveHistoryService); const notebookEditorService = accessor.get(INotebookEditorService); let editorControl: { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; + let isReplEditor = false; if (context) { const resourceUri = URI.revive(context); - const editors = editorService.findEditors(resourceUri) - .filter(id => id.editor instanceof InteractiveEditorInput && id.editor.resource?.toString() === resourceUri.toString()); - if (editors.length) { - const editorInput = editors[0].editor as InteractiveEditorInput; - const currentGroup = editors[0].groupId; - const editor = await editorService.openEditor(editorInput, currentGroup); - editorControl = editor?.getControl() as { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; + const editors = editorService.findEditors(resourceUri); + for (const found of editors) { + if (found.editor.typeId === ReplEditorInput.ID || found.editor.typeId === InteractiveEditorInput.ID) { + const editor = await editorService.openEditor(found.editor, found.groupId); + editorControl = editor?.getControl() as { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; + isReplEditor = found.editor.typeId === ReplEditorInput.ID; + break; + } } } else { + const editor = editorService.activeEditorPane; + isReplEditor = editor instanceof ReplEditor; editorControl = editorService.activeEditorPane?.getControl() as { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; } + if (editorControl && isReplEditor) { + executeReplInput(accessor, editorControl); + } + if (editorControl && editorControl.notebookEditor && editorControl.codeEditor) { const notebookDocument = editorControl.notebookEditor.textModel; const textModel = editorControl.codeEditor.getModel(); @@ -590,7 +621,7 @@ registerAction2(class extends Action2 { title: localize2('interactive.history.previous', 'Previous value in history'), category: interactiveWindowCategory, f1: false, - keybinding: { + keybinding: [{ when: ContextKeyExpr.and( ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('bottom'), @@ -599,7 +630,16 @@ registerAction2(class extends Action2 { ), primary: KeyCode.UpArrow, weight: KeybindingWeight.WorkbenchContrib - }, + }, { + when: ContextKeyExpr.and( + ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl'), + INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('bottom'), + INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('none'), + SuggestContext.Visible.toNegated() + ), + primary: KeyCode.UpArrow, + weight: KeybindingWeight.WorkbenchContrib + }] }); } @@ -629,7 +669,7 @@ registerAction2(class extends Action2 { title: localize2('interactive.history.next', 'Next value in history'), category: interactiveWindowCategory, f1: false, - keybinding: { + keybinding: [{ when: ContextKeyExpr.and( ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('top'), @@ -638,7 +678,16 @@ registerAction2(class extends Action2 { ), primary: KeyCode.DownArrow, weight: KeybindingWeight.WorkbenchContrib - }, + }, { + when: ContextKeyExpr.and( + ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl'), + INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('top'), + INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('none'), + SuggestContext.Visible.toNegated() + ), + primary: KeyCode.DownArrow, + weight: KeybindingWeight.WorkbenchContrib + }], }); } @@ -814,10 +863,17 @@ Registry.as(ConfigurationExtensions.Configuration).regis default: false, markdownDescription: localize('interactiveWindow.promptToSaveOnClose', "Prompt to save the interactive window when it is closed. Only new interactive windows will be affected by this setting change.") }, - ['interactiveWindow.executeWithShiftEnter']: { + [InteractiveWindowSetting.executeWithShiftEnter]: { + type: 'boolean', + default: false, + markdownDescription: localize('interactiveWindow.executeWithShiftEnter', "Execute the Interactive Window (REPL) input box with shift+enter, so that enter can be used to create a newline."), + tags: ['replExecute'] + }, + [InteractiveWindowSetting.showExecutionHint]: { type: 'boolean', default: true, - markdownDescription: localize('interactiveWindow.executeWithShiftEnter', "Execute the interactive window (REPL) input box with shift+enter, so that enter can be used to create a newline.") + markdownDescription: localize('interactiveWindow.showExecutionHint', "Display a hint in the Interactive Window (REPL) input box to indicate how to execute code."), + tags: ['replExecute'] } } }); diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveCommon.ts b/src/vs/workbench/contrib/interactive/browser/interactiveCommon.ts index 46a52d9b844..20ce01d8e34 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveCommon.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveCommon.ts @@ -8,5 +8,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const INTERACTIVE_INPUT_CURSOR_BOUNDARY = new RawContextKey<'none' | 'top' | 'bottom' | 'both'>('interactiveInputCursorAtBoundary', 'none'); export const InteractiveWindowSetting = { - interactiveWindowAlwaysScrollOnNewCell: 'interactiveWindow.alwaysScrollOnNewCell' + interactiveWindowAlwaysScrollOnNewCell: 'interactiveWindow.alwaysScrollOnNewCell', + executeWithShiftEnter: 'interactiveWindow.executeWithShiftEnter', + showExecutionHint: 'interactiveWindow.showExecutionHint' }; diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index ba994f540b6..a6b48582111 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -4,19 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/interactive'; -import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; -import { ICodeEditorViewState, IDecorationOptions } from 'vs/editor/common/editorCommon'; +import { ICodeEditorViewState } from 'vs/editor/common/editorCommon'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorPaneSelectionChangeReason, IEditorMemento, IEditorOpenContext, IEditorPaneScrollPosition, IEditorPaneSelectionChangeEvent, IEditorPaneWithScrolling } from 'vs/workbench/common/editor'; @@ -63,6 +61,7 @@ import 'vs/css!./interactiveEditor'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { deepClone } from 'vs/base/common/objects'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ReplInputHintContentWidget } from 'vs/workbench/contrib/interactive/browser/replInputHintContentWidget'; const DECORATION_KEY = 'interactiveInputDecoration'; const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; @@ -71,6 +70,7 @@ const INPUT_CELL_VERTICAL_PADDING = 8; const INPUT_CELL_HORIZONTAL_PADDING_RIGHT = 10; const INPUT_EDITOR_PADDING = 8; + export interface InteractiveEditorViewState { readonly notebook?: INotebookEditorViewState; readonly input?: ICodeEditorViewState | null; @@ -88,6 +88,7 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro private _inputCellContainer!: HTMLElement; private _inputFocusIndicator!: HTMLElement; private _inputRunButtonContainer!: HTMLElement; + private _inputConfigContainer!: HTMLElement; private _inputEditorContainer!: HTMLElement; private _codeEditorWidget!: CodeEditorWidget; private _notebookWidgetService: INotebookEditorService; @@ -109,6 +110,7 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro private _editorMemento: IEditorMemento; private readonly _groupListener = this._register(new MutableDisposable()); private _runbuttonToolbar: ToolBar | undefined; + private _hintElement: ReplInputHintContentWidget | undefined; private _onDidFocusWidget = this._register(new Emitter()); override get onDidFocus(): Event { return this._onDidFocusWidget.event; } @@ -163,11 +165,11 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro this._editorOptions = this._computeEditorOptions(); } })); - this._notebookOptions = new NotebookOptions(this.window, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); + this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); - this._register(this._keybindingService.onDidUpdateKeybindings(this._updateInputDecoration, this)); + this._register(this._keybindingService.onDidUpdateKeybindings(this._updateInputHint, this)); this._register(this._notebookExecutionStateService.onDidChangeExecution((e) => { if (e.type === NotebookExecutionType.cell && isEqual(e.notebook, this._notebookWidget.value?.viewModel?.notebookDocument.uri)) { const cell = this._notebookWidget.value?.getCellByHandle(e.cellHandle); @@ -197,9 +199,36 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro this._inputRunButtonContainer = DOM.append(this._inputCellContainer, DOM.$('.run-button-container')); this._setupRunButtonToolbar(this._inputRunButtonContainer); this._inputEditorContainer = DOM.append(this._inputCellContainer, DOM.$('.input-editor-container')); + this._setupConfigButtonToolbar(); this._createLayoutStyles(); } + private _setupConfigButtonToolbar() { + this._inputConfigContainer = DOM.append(this._inputEditorContainer, DOM.$('.input-toolbar-container')); + this._inputConfigContainer.style.position = 'absolute'; + this._inputConfigContainer.style.right = '0'; + this._inputConfigContainer.style.marginTop = '6px'; + this._inputConfigContainer.style.marginRight = '12px'; + this._inputConfigContainer.style.zIndex = '1'; + this._inputConfigContainer.style.display = 'none'; + + const menu = this._register(this._menuService.createMenu(MenuId.InteractiveInputConfig, this._contextKeyService)); + const toolbar = this._register(new ToolBar(this._inputConfigContainer, this._contextMenuService, { + getKeyBinding: action => this._keybindingService.lookupKeybinding(action.id), + actionViewItemProvider: (action, options) => { + return createActionViewItem(this._instantiationService, action, options); + }, + renderDropdownAsChildElement: true + })); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, result); + toolbar.setActions([...primary, ...secondary]); + } + private _setupRunButtonToolbar(runButtonContainer: HTMLElement) { const menu = this._register(this._menuService.createMenu(MenuId.InteractiveInputExecute, this._contextKeyService)); this._runbuttonToolbar = this._register(new ToolBar(runButtonContainer, this._contextMenuService, { @@ -485,16 +514,26 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro this._widgetDisposableStore.add(this.themeService.onDidColorThemeChange(() => { if (this.isVisible()) { - this._updateInputDecoration(); + this._updateInputHint(); } })); this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeModelContent(() => { if (this.isVisible()) { - this._updateInputDecoration(); + this._updateInputHint(); } })); + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeModel(() => { + this._updateInputHint(); + })); + + this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(InteractiveWindowSetting.showExecutionHint)) { + this._updateInputHint(); + } + }); + const cursorAtBoundaryContext = INTERACTIVE_INPUT_CURSOR_BOUNDARY.bindTo(this._contextKeyService); if (input.resource && input.historyService.has(input.resource)) { cursorAtBoundaryContext.set('top'); @@ -535,6 +574,8 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro this._widgetDisposableStore.add(this._notebookWidget.value!.onDidScroll(() => this._onDidChangeScroll.fire())); this._syncWithKernel(); + + this._updateInputHint(); } override setOptions(options: INotebookEditorOptions | undefined): void { @@ -591,8 +632,6 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro NOTEBOOK_KERNEL.bindTo(this._contextKeyService).set(selectedOrSuggested.id); } } - - this._updateInputDecoration(); } layout(dimension: DOM.Dimension, position: DOM.IDomPosition): void { @@ -632,41 +671,24 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro return new DOM.Dimension(Math.max(0, width), Math.max(0, height)); } - private _updateInputDecoration(): void { + private _updateInputHint(): void { if (!this._codeEditorWidget) { return; } - if (!this._codeEditorWidget.hasModel()) { - return; + const shouldHide = + !this._codeEditorWidget.hasModel() || + this._configurationService.getValue(InteractiveWindowSetting.showExecutionHint) === false || + this._codeEditorWidget.getModel()!.getValueLength() !== 0; + + if (!this._hintElement && !shouldHide) { + this._hintElement = this._instantiationService.createInstance(ReplInputHintContentWidget, this._codeEditorWidget); + this._inputConfigContainer.style.display = 'block'; + } else if (this._hintElement && shouldHide) { + this._hintElement.dispose(); + this._hintElement = undefined; + this._inputConfigContainer.style.display = 'none'; } - - const model = this._codeEditorWidget.getModel(); - - const decorations: IDecorationOptions[] = []; - - if (model?.getValueLength() === 0) { - const transparentForeground = resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4); - const languageId = model.getLanguageId(); - const keybinding = this._keybindingService.lookupKeybinding('interactive.execute', this._contextKeyService)?.getLabel(); - const text = nls.localize('interactiveInputPlaceHolder', "Type '{0}' code here and press {1} to run", languageId, keybinding ?? 'ctrl+enter'); - decorations.push({ - range: { - startLineNumber: 0, - endLineNumber: 0, - startColumn: 0, - endColumn: 1 - }, - renderOptions: { - after: { - contentText: text, - color: transparentForeground ? transparentForeground.toString() : undefined - } - } - }); - } - - this._codeEditorWidget.setDecorationsByType('interactive-decoration', DECORATION_KEY, decorations); } getScrollPosition(): IEditorPaneScrollPosition { @@ -701,6 +723,8 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro this._notebookWidget.value.onWillHide(); } } + + this._updateInputHint(); } override clearInput() { diff --git a/src/vs/workbench/contrib/interactive/browser/replInputHintContentWidget.ts b/src/vs/workbench/contrib/interactive/browser/replInputHintContentWidget.ts new file mode 100644 index 00000000000..6dc4644b71a --- /dev/null +++ b/src/vs/workbench/contrib/interactive/browser/replInputHintContentWidget.ts @@ -0,0 +1,156 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { status } from 'vs/base/browser/ui/aria/aria'; +import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; +import { Event } from 'vs/base/common/event'; +import { ResolvedKeybinding } from 'vs/base/common/keybindings'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { OS } from 'vs/base/common/platform'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { InteractiveWindowSetting } from 'vs/workbench/contrib/interactive/browser/interactiveCommon'; + + +export class ReplInputHintContentWidget extends Disposable implements IContentWidget { + + private static readonly ID = 'replInput.widget.emptyHint'; + + private domNode: HTMLElement | undefined; + private ariaLabel: string = ''; + + constructor( + private readonly editor: ICodeEditor, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + ) { + super(); + + this._register(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + if (this.domNode && e.hasChanged(EditorOption.fontInfo)) { + this.editor.applyFontInfo(this.domNode); + } + })); + const onDidFocusEditorText = Event.debounce(this.editor.onDidFocusEditorText, () => undefined, 500); + this._register(onDidFocusEditorText(() => { + if (this.editor.hasTextFocus() && this.ariaLabel && configurationService.getValue(AccessibilityVerbositySettingId.ReplInputHint)) { + status(this.ariaLabel); + } + })); + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(InteractiveWindowSetting.executeWithShiftEnter)) { + this.setHint(); + } + })); + this.editor.addContentWidget(this); + } + + getId(): string { + return ReplInputHintContentWidget.ID; + } + + getPosition(): IContentWidgetPosition | null { + return { + position: { lineNumber: 1, column: 1 }, + preference: [ContentWidgetPositionPreference.EXACT] + }; + } + + getDomNode(): HTMLElement { + if (!this.domNode) { + this.domNode = dom.$('.empty-editor-hint'); + this.domNode.style.width = 'max-content'; + this.domNode.style.paddingLeft = '4px'; + + this.setHint(); + + this._register(dom.addDisposableListener(this.domNode, 'click', () => { + this.editor.focus(); + })); + + this.editor.applyFontInfo(this.domNode); + } + + return this.domNode; + } + + private setHint() { + if (!this.domNode) { + return; + } + while (this.domNode.firstChild) { + this.domNode.removeChild(this.domNode.firstChild); + } + + const hintElement = dom.$('div.empty-hint-text'); + hintElement.style.cursor = 'text'; + hintElement.style.whiteSpace = 'nowrap'; + + const keybinding = this.getKeybinding(); + const keybindingHintLabel = keybinding?.getLabel(); + + if (keybinding && keybindingHintLabel) { + const actionPart = localize('emptyHintText', 'Press {0} to execute. ', keybindingHintLabel); + + const [before, after] = actionPart.split(keybindingHintLabel).map((fragment) => { + const hintPart = dom.$('span', undefined, fragment); + hintPart.style.fontStyle = 'italic'; + return hintPart; + }); + + hintElement.appendChild(before); + + const label = new KeybindingLabel(hintElement, OS); + label.set(keybinding); + label.element.style.width = 'min-content'; + label.element.style.display = 'inline'; + + hintElement.appendChild(after); + this.domNode.append(hintElement); + + this.ariaLabel = actionPart.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.ReplInputHint)); + } + } + + private getKeybinding() { + const keybindings = this.keybindingService.lookupKeybindings('interactive.execute'); + const shiftEnterConfig = this.configurationService.getValue(InteractiveWindowSetting.executeWithShiftEnter); + const hasEnterChord = (kb: ResolvedKeybinding, modifier: string = '') => { + const chords = kb.getDispatchChords(); + const chord = modifier + 'Enter'; + const chordAlt = modifier + '[Enter]'; + return chords.length === 1 && (chords[0] === chord || chords[0] === chordAlt); + }; + + if (shiftEnterConfig) { + const keybinding = keybindings.find(kb => hasEnterChord(kb, 'shift+')); + if (keybinding) { + return keybinding; + } + } else { + let keybinding = keybindings.find(kb => hasEnterChord(kb)); + if (keybinding) { + return keybinding; + } + keybinding = this.keybindingService.lookupKeybindings('python.execInREPLEnter') + .find(kb => hasEnterChord(kb)); + if (keybinding) { + return keybinding; + } + } + + return keybindings?.[0]; + } + + override dispose(): void { + super.dispose(); + this.editor.removeContentWidget(this); + } +} diff --git a/src/vs/workbench/contrib/issue/browser/issue.ts b/src/vs/workbench/contrib/issue/browser/issue.ts index 79e44855c34..3bc09a5a6c6 100644 --- a/src/vs/workbench/contrib/issue/browser/issue.ts +++ b/src/vs/workbench/contrib/issue/browser/issue.ts @@ -240,26 +240,6 @@ export class BaseIssueReporterService extends Disposable { } public setEventHandlers(): void { - this.addEventListener('issue-type', 'change', (event: Event) => { - const issueType = parseInt((event.target).value); - this.issueReporterModel.update({ issueType: issueType }); - if (issueType === IssueType.PerformanceIssue && !this.receivedPerformanceInfo) { - this.issueMainService.$getPerformanceInfo().then(info => { - this.updatePerformanceInfo(info as Partial); - }); - } - - // Resets placeholder - const descriptionTextArea = this.getElementById('issue-title'); - if (descriptionTextArea) { - descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title"); - } - - this.updatePreviewButtonState(); - this.setSourceOptions(); - this.render(); - }); - (['includeSystemInfo', 'includeProcessInfo', 'includeWorkspaceInfo', 'includeExtensions', 'includeExperiments', 'includeExtensionData'] as const).forEach(elementId => { this.addEventListener(elementId, 'click', (event: Event) => { event.stopPropagation(); @@ -1076,7 +1056,7 @@ export class BaseIssueReporterService extends Disposable { const showLoading = this.getElementById('ext-loading')!; show(showLoading); while (showLoading.firstChild) { - showLoading.removeChild(showLoading.firstChild); + showLoading.firstChild.remove(); } showLoading.append(element); @@ -1097,7 +1077,7 @@ export class BaseIssueReporterService extends Disposable { const hideLoading = this.getElementById('ext-loading')!; hide(hideLoading); if (hideLoading.firstChild) { - hideLoading.removeChild(element); + element.remove(); } this.renderBlocks(); } @@ -1202,5 +1182,3 @@ export function hide(el: Element | undefined | null) { export function show(el: Element | undefined | null) { el?.classList.remove('hidden'); } - - diff --git a/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts b/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts index 37d52199b5b..61ca99ac1c2 100644 --- a/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts +++ b/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts @@ -11,7 +11,7 @@ import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ThemeIcon } from 'vs/base/common/themables'; import { Codicon } from 'vs/base/common/codicons'; import { IssueSource } from 'vs/platform/issue/common/issue'; @@ -107,7 +107,7 @@ export class IssueQuickAccess extends PickerQuickAccessProvider { + const issueType = parseInt((event.target).value); + this.issueReporterModel.update({ issueType: issueType }); + + // Resets placeholder + const descriptionTextArea = this.getElementById('issue-title'); + if (descriptionTextArea) { + descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title"); + } + + this.updatePreviewButtonState(); + this.setSourceOptions(); + this.render(); + }); this.previewButton.onDidClick(async () => { this.delayedSubmit.trigger(async () => { this.createIssue(); diff --git a/src/vs/workbench/contrib/issue/common/issue.ts b/src/vs/workbench/contrib/issue/common/issue.ts index 3ecd103cf58..5834ba051be 100644 --- a/src/vs/workbench/contrib/issue/common/issue.ts +++ b/src/vs/workbench/contrib/issue/common/issue.ts @@ -10,5 +10,12 @@ export const IWorkbenchIssueService = createDecorator('w export interface IWorkbenchIssueService { readonly _serviceBrand: undefined; openReporter(dataOverrides?: Partial): Promise; +} + +export const IWorkbenchProcessService = createDecorator('workbenchProcessService'); + +export interface IWorkbenchProcessService { + readonly _serviceBrand: undefined; openProcessExplorer(): Promise; } + diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts index 3b86046d12f..9eacc2f7574 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { localize, localize2 } from 'vs/nls'; -import { MenuRegistry, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { BaseIssueContribution } from 'vs/workbench/contrib/issue/common/issue.contribution'; import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -14,11 +13,7 @@ import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { INativeHostService } from 'vs/platform/native/common/native'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { IIssueMainService, IssueType } from 'vs/platform/issue/common/issue'; +import { IssueType } from 'vs/platform/issue/common/issue'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; @@ -27,7 +22,6 @@ import 'vs/workbench/contrib/issue/electron-sandbox/issueMainService'; import 'vs/workbench/contrib/issue/electron-sandbox/issueService'; import 'vs/workbench/contrib/issue/browser/issueTroubleshoot'; - //#region Issue Contribution class NativeIssueContribution extends BaseIssueContribution { @@ -87,87 +81,10 @@ class ReportPerformanceIssueUsingReporterAction extends Action2 { } override async run(accessor: ServicesAccessor): Promise { - const issueService = accessor.get(IWorkbenchIssueService); + const issueService = accessor.get(IWorkbenchIssueService); // later can just get IIssueFormService return issueService.openReporter({ issueType: IssueType.PerformanceIssue }); } } -//#endregion - -//#region Commands - -class OpenProcessExplorer extends Action2 { - - static readonly ID = 'workbench.action.openProcessExplorer'; - - constructor() { - super({ - id: OpenProcessExplorer.ID, - title: localize2('openProcessExplorer', 'Open Process Explorer'), - category: Categories.Developer, - f1: true - }); - } - - override async run(accessor: ServicesAccessor): Promise { - const issueService = accessor.get(IWorkbenchIssueService); - - return issueService.openProcessExplorer(); - } -} -registerAction2(OpenProcessExplorer); -MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '5_tools', - command: { - id: OpenProcessExplorer.ID, - title: localize({ key: 'miOpenProcessExplorerer', comment: ['&& denotes a mnemonic'] }, "Open &&Process Explorer") - }, - order: 2 -}); - -class StopTracing extends Action2 { - - static readonly ID = 'workbench.action.stopTracing'; - - constructor() { - super({ - id: StopTracing.ID, - title: localize2('stopTracing', 'Stop Tracing'), - category: Categories.Developer, - f1: true - }); - } - - override async run(accessor: ServicesAccessor): Promise { - const issueService = accessor.get(IIssueMainService); - const environmentService = accessor.get(INativeEnvironmentService); - const dialogService = accessor.get(IDialogService); - const nativeHostService = accessor.get(INativeHostService); - const progressService = accessor.get(IProgressService); - - if (!environmentService.args.trace) { - const { confirmed } = await dialogService.confirm({ - message: localize('stopTracing.message', "Tracing requires to launch with a '--trace' argument"), - primaryButton: localize({ key: 'stopTracing.button', comment: ['&& denotes a mnemonic'] }, "&&Relaunch and Enable Tracing"), - }); - - if (confirmed) { - return nativeHostService.relaunch({ addArgs: ['--trace'] }); - } - } - - await progressService.withProgress({ - location: ProgressLocation.Dialog, - title: localize('stopTracing.title', "Creating trace file..."), - cancellable: false, - detail: localize('stopTracing.detail', "This can take up to one minute to complete.") - }, () => issueService.stopTracing()); - } -} -registerAction2(StopTracing); - -CommandsRegistry.registerCommand('_issues.getSystemStatus', (accessor) => { - return accessor.get(IIssueMainService).getSystemStatus(); -}); -//#endregion +// #endregion diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueMainService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueMainService.ts index a3cb28473af..cf16313519a 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueMainService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueMainService.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; -import { IIssueMainService } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, IProcessMainService } from 'vs/platform/issue/common/issue'; registerMainProcessRemoteService(IIssueMainService, 'issue'); +registerMainProcessRemoteService(IProcessMainService, 'process'); + diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js index cad5ddba090..aad5671f1f0 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js @@ -7,6 +7,10 @@ (function () { 'use strict'; + /** + * @import { ISandboxConfiguration } from '../../../../base/parts/sandbox/common/sandboxTypes' + */ + const bootstrapWindow = bootstrapWindowLib(); // Load issue reporter into window @@ -24,12 +28,10 @@ ); /** - * @typedef {import('../../../../base/parts/sandbox/common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration - * * @returns {{ * load: ( * modules: string[], - * resultCallback: (result, configuration: ISandboxConfiguration) => unknown, + * resultCallback: (result: any, configuration: ISandboxConfiguration) => unknown, * options?: { * configureDeveloperSettings?: (config: ISandboxConfiguration) => { * forceEnableDeveloperKeybindings?: boolean, diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts index 05f002de532..ca9253bb1e9 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts @@ -15,12 +15,13 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService'; import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; -import { IIssueMainService, IssueReporterWindowConfiguration } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, IProcessMainService, IssueReporterWindowConfiguration } from 'vs/platform/issue/common/issue'; import { INativeHostService } from 'vs/platform/native/common/native'; import { NativeHostService } from 'vs/platform/native/common/nativeHostService'; import { IssueReporter2 } from 'vs/workbench/contrib/issue/electron-sandbox/issueReporterService2'; import { mainWindow } from 'vs/base/browser/window'; + export function startup(configuration: IssueReporterWindowConfiguration) { const platformClass = isWindows ? 'windows' : isLinux ? 'linux' : 'mac'; mainWindow.document.body.classList.add(platformClass); // used by our fonts @@ -50,3 +51,4 @@ function initServices(windowId: number) { } registerMainProcessRemoteService(IIssueMainService, 'issue'); +registerMainProcessRemoteService(IProcessMainService, 'process'); diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts index e3900cac953..3e701d670a9 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts @@ -19,7 +19,7 @@ import { URI } from 'vs/base/common/uri'; import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/workbench/contrib/issue/browser/issueReporterModel'; import { localize } from 'vs/nls'; import { isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; -import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, IProcessMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; import { INativeHostService } from 'vs/platform/native/common/native'; import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet'; @@ -59,7 +59,8 @@ export class IssueReporter extends Disposable { constructor( private readonly configuration: IssueReporterWindowConfiguration, @INativeHostService private readonly nativeHostService: INativeHostService, - @IIssueMainService private readonly issueMainService: IIssueMainService + @IIssueMainService private readonly issueMainService: IIssueMainService, + @IProcessMainService private readonly processMainService: IProcessMainService ) { super(); const targetExtension = configuration.data.extensionId ? configuration.data.enabledExtensions.find(extension => extension.id.toLocaleLowerCase() === configuration.data.extensionId?.toLocaleLowerCase()) : undefined; @@ -107,7 +108,7 @@ export class IssueReporter extends Disposable { } } - this.issueMainService.$getSystemInfo().then(info => { + this.processMainService.$getSystemInfo().then(info => { this.issueReporterModel.update({ systemInfo: info }); this.receivedSystemInfo = true; @@ -115,7 +116,7 @@ export class IssueReporter extends Disposable { this.updatePreviewButtonState(); }); if (configuration.data.issueType === IssueType.PerformanceIssue) { - this.issueMainService.$getPerformanceInfo().then(info => { + this.processMainService.$getPerformanceInfo().then(info => { this.updatePerformanceInfo(info as Partial); }); } @@ -286,7 +287,7 @@ export class IssueReporter extends Disposable { const issueType = parseInt((event.target).value); this.issueReporterModel.update({ issueType: issueType }); if (issueType === IssueType.PerformanceIssue && !this.receivedPerformanceInfo) { - this.issueMainService.$getPerformanceInfo().then(info => { + this.processMainService.$getPerformanceInfo().then(info => { this.updatePerformanceInfo(info as Partial); }); } @@ -1386,7 +1387,7 @@ export class IssueReporter extends Disposable { const showLoading = this.getElementById('ext-loading')!; show(showLoading); while (showLoading.firstChild) { - showLoading.removeChild(showLoading.firstChild); + showLoading.firstChild.remove(); } showLoading.append(element); @@ -1407,7 +1408,7 @@ export class IssueReporter extends Disposable { const hideLoading = this.getElementById('ext-loading')!; hide(hideLoading); if (hideLoading.firstChild) { - hideLoading.removeChild(element); + element.remove(); } this.renderBlocks(); } diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts index 6f6bdd1c1f4..abb6680a5d2 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts @@ -12,7 +12,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; -import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, IProcessMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; import { INativeHostService } from 'vs/platform/native/common/native'; import { applyZoom, zoomIn, zoomOut } from 'vs/platform/window/electron-sandbox/window'; import { BaseIssueReporterService, hide, show } from 'vs/workbench/contrib/issue/browser/issue'; @@ -24,14 +24,17 @@ const MAX_URL_LENGTH = 7500; export class IssueReporter2 extends BaseIssueReporterService { + private readonly processMainService: IProcessMainService; constructor( private readonly configuration: IssueReporterWindowConfiguration, @INativeHostService private readonly nativeHostService: INativeHostService, - @IIssueMainService issueMainService: IIssueMainService + @IIssueMainService issueMainService: IIssueMainService, + @IProcessMainService processMainService: IProcessMainService ) { super(configuration.disableExtensions, configuration.data, configuration.os, configuration.product, mainWindow, false, issueMainService); - this.issueMainService.$getSystemInfo().then(info => { + this.processMainService = processMainService; + this.processMainService.$getSystemInfo().then(info => { this.issueReporterModel.update({ systemInfo: info }); this.receivedSystemInfo = true; @@ -39,7 +42,7 @@ export class IssueReporter2 extends BaseIssueReporterService { this.updatePreviewButtonState(); }); if (configuration.data.issueType === IssueType.PerformanceIssue) { - this.issueMainService.$getPerformanceInfo().then(info => { + this.processMainService.$getPerformanceInfo().then(info => { this.updatePerformanceInfo(info as Partial); }); } @@ -81,6 +84,26 @@ export class IssueReporter2 extends BaseIssueReporterService { public override setEventHandlers(): void { super.setEventHandlers(); + this.addEventListener('issue-type', 'change', (event: Event) => { + const issueType = parseInt((event.target).value); + this.issueReporterModel.update({ issueType: issueType }); + if (issueType === IssueType.PerformanceIssue && !this.receivedPerformanceInfo) { + this.processMainService.$getPerformanceInfo().then(info => { + this.updatePerformanceInfo(info as Partial); + }); + } + + // Resets placeholder + const descriptionTextArea = this.getElementById('issue-title'); + if (descriptionTextArea) { + descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title"); + } + + this.updatePreviewButtonState(); + this.setSourceOptions(); + this.render(); + }); + // Keep all event listerns involving window and issue creation this.previewButton.onDidClick(async () => { this.delayedSubmit.trigger(async () => { @@ -464,7 +487,7 @@ export class IssueReporter2 extends BaseIssueReporterService { const showLoading = this.getElementById('ext-loading')!; show(showLoading); while (showLoading.firstChild) { - showLoading.removeChild(showLoading.firstChild); + showLoading.firstChild.remove(); } showLoading.append(element); @@ -485,7 +508,7 @@ export class IssueReporter2 extends BaseIssueReporterService { const hideLoading = this.getElementById('ext-loading')!; hide(hideLoading); if (hideLoading.firstChild) { - hideLoading.removeChild(element); + element.remove(); } this.renderBlocks(); } diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts index 3b5c8d26ed6..9986f5065f0 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts @@ -4,20 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { getZoomLevel } from 'vs/base/browser/browser'; -import { platform } from 'vs/base/common/process'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionIdentifier, ExtensionType, ExtensionIdentifierSet } from 'vs/platform/extensions/common/extensions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, ProcessExplorerData } from 'vs/platform/issue/common/issue'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { activeContrastBorder, buttonBackground, buttonForeground, buttonHoverBackground, editorBackground, editorForeground, foreground, inputActiveOptionBorder, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, listActiveSelectionBackground, listActiveSelectionForeground, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles } from 'vs/platform/issue/common/issue'; +import { buttonBackground, buttonForeground, buttonHoverBackground, foreground, inputActiveOptionBorder, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; -import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; @@ -34,9 +31,7 @@ export class NativeIssueService implements IWorkbenchIssueService { @IThemeService private readonly themeService: IThemeService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, - @IProductService private readonly productService: IProductService, @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IIntegrityService private readonly integrityService: IIntegrityService, @@ -153,34 +148,6 @@ export class NativeIssueService implements IWorkbenchIssueService { return this.issueMainService.openReporter(issueReporterData); } - openProcessExplorer(): Promise { - const theme = this.themeService.getColorTheme(); - const data: ProcessExplorerData = { - pid: this.environmentService.mainPid, - zoomLevel: getZoomLevel(mainWindow), - styles: { - backgroundColor: getColor(theme, editorBackground), - color: getColor(theme, editorForeground), - listHoverBackground: getColor(theme, listHoverBackground), - listHoverForeground: getColor(theme, listHoverForeground), - listFocusBackground: getColor(theme, listFocusBackground), - listFocusForeground: getColor(theme, listFocusForeground), - listFocusOutline: getColor(theme, listFocusOutline), - listActiveSelectionBackground: getColor(theme, listActiveSelectionBackground), - listActiveSelectionForeground: getColor(theme, listActiveSelectionForeground), - listHoverOutline: getColor(theme, activeContrastBorder), - scrollbarShadowColor: getColor(theme, scrollbarShadow), - scrollbarSliderActiveBackgroundColor: getColor(theme, scrollbarSliderActiveBackground), - scrollbarSliderBackgroundColor: getColor(theme, scrollbarSliderBackground), - scrollbarSliderHoverBackgroundColor: getColor(theme, scrollbarSliderHoverBackground), - }, - platform: platform, - applicationName: this.productService.applicationName - }; - return this.issueMainService.openProcessExplorer(data); - } - - } export function getIssueReporterStyles(theme: IColorTheme): IssueReporterStyles { diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/process.contribution.ts b/src/vs/workbench/contrib/issue/electron-sandbox/process.contribution.ts new file mode 100644 index 00000000000..43fe8fb39d2 --- /dev/null +++ b/src/vs/workbench/contrib/issue/electron-sandbox/process.contribution.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize, localize2 } from 'vs/nls'; +import { MenuRegistry, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { IWorkbenchProcessService } from 'vs/workbench/contrib/issue/common/issue'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { INativeHostService } from 'vs/platform/native/common/native'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProcessMainService } from 'vs/platform/issue/common/issue'; +import 'vs/workbench/contrib/issue/electron-sandbox/processService'; +import 'vs/workbench/contrib/issue/electron-sandbox/issueMainService'; + + +//#region Commands + +class OpenProcessExplorer extends Action2 { + + static readonly ID = 'workbench.action.openProcessExplorer'; + + constructor() { + super({ + id: OpenProcessExplorer.ID, + title: localize2('openProcessExplorer', 'Open Process Explorer'), + category: Categories.Developer, + f1: true + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const processService = accessor.get(IWorkbenchProcessService); + + return processService.openProcessExplorer(); + } +} +registerAction2(OpenProcessExplorer); +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '5_tools', + command: { + id: OpenProcessExplorer.ID, + title: localize({ key: 'miOpenProcessExplorerer', comment: ['&& denotes a mnemonic'] }, "Open &&Process Explorer") + }, + order: 2 +}); + +class StopTracing extends Action2 { + + static readonly ID = 'workbench.action.stopTracing'; + + constructor() { + super({ + id: StopTracing.ID, + title: localize2('stopTracing', 'Stop Tracing'), + category: Categories.Developer, + f1: true + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const processService = accessor.get(IProcessMainService); + const environmentService = accessor.get(INativeEnvironmentService); + const dialogService = accessor.get(IDialogService); + const nativeHostService = accessor.get(INativeHostService); + const progressService = accessor.get(IProgressService); + + if (!environmentService.args.trace) { + const { confirmed } = await dialogService.confirm({ + message: localize('stopTracing.message', "Tracing requires to launch with a '--trace' argument"), + primaryButton: localize({ key: 'stopTracing.button', comment: ['&& denotes a mnemonic'] }, "&&Relaunch and Enable Tracing"), + }); + + if (confirmed) { + return nativeHostService.relaunch({ addArgs: ['--trace'] }); + } + } + + await progressService.withProgress({ + location: ProgressLocation.Dialog, + title: localize('stopTracing.title', "Creating trace file..."), + cancellable: false, + detail: localize('stopTracing.detail', "This can take up to one minute to complete.") + }, () => processService.stopTracing()); + } +} +registerAction2(StopTracing); + +CommandsRegistry.registerCommand('_issues.getSystemStatus', (accessor) => { + return accessor.get(IProcessMainService).getSystemStatus(); +}); +//#endregion diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/processService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/processService.ts new file mode 100644 index 00000000000..60ebd4f898f --- /dev/null +++ b/src/vs/workbench/contrib/issue/electron-sandbox/processService.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getZoomLevel } from 'vs/base/browser/browser'; +import { platform } from 'vs/base/common/process'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IProcessMainService, ProcessExplorerData } from 'vs/platform/issue/common/issue'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { activeContrastBorder, editorBackground, editorForeground, listActiveSelectionBackground, listActiveSelectionForeground, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { IWorkbenchProcessService } from 'vs/workbench/contrib/issue/common/issue'; +import { mainWindow } from 'vs/base/browser/window'; + +export class ProcessService implements IWorkbenchProcessService { + declare readonly _serviceBrand: undefined; + + constructor( + @IProcessMainService private readonly processMainService: IProcessMainService, + @IThemeService private readonly themeService: IThemeService, + @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, + @IProductService private readonly productService: IProductService, + ) { } + + openProcessExplorer(): Promise { + const theme = this.themeService.getColorTheme(); + const data: ProcessExplorerData = { + pid: this.environmentService.mainPid, + zoomLevel: getZoomLevel(mainWindow), + styles: { + backgroundColor: getColor(theme, editorBackground), + color: getColor(theme, editorForeground), + listHoverBackground: getColor(theme, listHoverBackground), + listHoverForeground: getColor(theme, listHoverForeground), + listFocusBackground: getColor(theme, listFocusBackground), + listFocusForeground: getColor(theme, listFocusForeground), + listFocusOutline: getColor(theme, listFocusOutline), + listActiveSelectionBackground: getColor(theme, listActiveSelectionBackground), + listActiveSelectionForeground: getColor(theme, listActiveSelectionForeground), + listHoverOutline: getColor(theme, activeContrastBorder), + scrollbarShadowColor: getColor(theme, scrollbarShadow), + scrollbarSliderActiveBackgroundColor: getColor(theme, scrollbarSliderActiveBackground), + scrollbarSliderBackgroundColor: getColor(theme, scrollbarSliderBackground), + scrollbarSliderHoverBackgroundColor: getColor(theme, scrollbarSliderHoverBackground), + }, + platform: platform, + applicationName: this.productService.applicationName + }; + return this.processMainService.openProcessExplorer(data); + } + + +} + +function getColor(theme: IColorTheme, key: string): string | undefined { + const color = theme.getColor(key); + return color ? color.toString() : undefined; +} + +registerSingleton(IWorkbenchProcessService, ProcessService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/issue/issue/testReporterModel.test.ts b/src/vs/workbench/contrib/issue/issue/testReporterModel.test.ts index d86022dec6a..60ea6c2089c 100644 --- a/src/vs/workbench/contrib/issue/issue/testReporterModel.test.ts +++ b/src/vs/workbench/contrib/issue/issue/testReporterModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IssueReporterModel } from 'vs/workbench/contrib/issue/browser/issueReporterModel'; import { IssueType } from 'vs/platform/issue/common/issue'; diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 3feb89c6616..daf83ca4cb0 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -122,7 +122,7 @@ class LanguageStatus { this._update(); this._storeState(); } - }, this._disposables); + }, undefined, this._disposables); } diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index 4354ad022df..25433bee4a4 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -113,6 +113,7 @@ .monaco-workbench .hover-language-status > .element .right .monaco-link { margin: auto 0; white-space: nowrap; + text-decoration: var(--text-link-decoration); } .monaco-workbench .hover-language-status > .element .right .monaco-action-bar:not(:first-child) { diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index b96ba30f68c..a5c42063463 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -70,11 +70,11 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { super(); const contextKey = CONTEXT_LOG_LEVEL.bindTo(contextKeyService); contextKey.set(LogLevelToString(loggerService.getLogLevel())); - loggerService.onDidChangeLogLevel(e => { + this._register(loggerService.onDidChangeLogLevel(e => { if (isLogLevel(e)) { contextKey.set(LogLevelToString(loggerService.getLogLevel())); } - }); + })); this.onDidAddLoggers(loggerService.getRegisteredLoggers()); this._register(loggerService.onDidChangeLoggers(({ added, removed }) => { diff --git a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index 9b18b5cd028..b1c6b962fab 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts @@ -13,7 +13,6 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { tokenizeToString } from 'vs/editor/common/languages/textToHtmlTokenizer'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { escape } from 'vs/base/common/strings'; -import { SimpleSettingRenderer } from 'vs/workbench/contrib/markdown/browser/markdownSettingRenderer'; export const DEFAULT_MARKDOWN_STYLES = ` body { @@ -33,7 +32,7 @@ img { } a { - text-decoration: none; + text-decoration: var(--text-link-decoration); } a:hover { @@ -184,6 +183,13 @@ function sanitize(documentContent: string, allowUnknownProtocols: boolean): stri } } +interface IRenderMarkdownDocumentOptions { + readonly shouldSanitize?: boolean; + readonly allowUnknownProtocols?: boolean; + readonly renderer?: marked.Renderer; + readonly token?: CancellationToken; +} + /** * Renders a string of markdown as a document. * @@ -193,10 +199,7 @@ export async function renderMarkdownDocument( text: string, extensionService: IExtensionService, languageService: ILanguageService, - shouldSanitize: boolean = true, - allowUnknownProtocols: boolean = false, - token?: CancellationToken, - settingRenderer?: SimpleSettingRenderer + options?: IRenderMarkdownDocumentOptions ): Promise { const highlight = (code: string, lang: string | undefined, callback: ((error: any, code: string) => void) | undefined): any => { @@ -210,7 +213,7 @@ export async function renderMarkdownDocument( } extensionService.whenInstalledExtensionsRegistered().then(async () => { - if (token?.isCancellationRequested) { + if (options?.token?.isCancellationRequested) { callback(null, ''); return; } @@ -222,16 +225,11 @@ export async function renderMarkdownDocument( return ''; }; - const renderer = new marked.Renderer(); - if (settingRenderer) { - renderer.html = settingRenderer.getHtmlRenderer(); - } - return new Promise((resolve, reject) => { - marked(text, { highlight, renderer }, (err, value) => err ? reject(err) : resolve(value)); + marked(text, { highlight, renderer: options?.renderer }, (err, value) => err ? reject(err) : resolve(value)); }).then(raw => { - if (shouldSanitize) { - return sanitize(raw, allowUnknownProtocols); + if (options?.shouldSanitize ?? true) { + return sanitize(raw, options?.allowUnknownProtocols ?? false); } else { return raw; } diff --git a/src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts b/src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts index 55e38f3d018..c0ee61a5161 100644 --- a/src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts +++ b/src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IAction } from 'vs/base/common/actions'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts b/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts index e7355a0dd3d..c97455cb4ed 100644 --- a/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts +++ b/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts @@ -111,7 +111,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis 'type': 'object', 'properties': { 'problems.decorations.enabled': { - 'markdownDescription': localize('markers.showOnFile', "Show Errors & Warnings on files and folder. Overwritten by `#problems.visibility#` when it is off."), + 'markdownDescription': localize('markers.showOnFile', "Show Errors & Warnings on files and folder. Overwritten by {0} when it is off.", '`#problems.visibility#`'), 'type': 'boolean', 'default': true } diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 40c99ee3457..1a3dec090cc 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -50,7 +50,7 @@ import { unsupportedSchemas } from 'vs/platform/markers/common/markerService'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import Severity from 'vs/base/common/severity'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; interface IResourceMarkersTemplateData { @@ -281,7 +281,7 @@ class MarkerWidget extends Disposable { private readonly icon: HTMLElement; private readonly iconContainer: HTMLElement; private readonly messageAndDetailsContainer: HTMLElement; - private readonly messageAndDetailsContainerHover: IUpdatableHover; + private readonly messageAndDetailsContainerHover: IManagedHover; private readonly disposables = this._register(new DisposableStore()); constructor( @@ -302,7 +302,7 @@ class MarkerWidget extends Disposable { this.iconContainer = dom.append(parent, dom.$('')); this.icon = dom.append(this.iconContainer, dom.$('')); this.messageAndDetailsContainer = dom.append(parent, dom.$('.marker-message-details-container')); - this.messageAndDetailsContainerHover = this._register(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.messageAndDetailsContainer, '')); + this.messageAndDetailsContainerHover = this._register(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.messageAndDetailsContainer, '')); } render(element: Marker, filterData: MarkerFilterData | undefined): void { diff --git a/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts b/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts index b8334c948d6..bec693957c3 100644 --- a/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts +++ b/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { IMarker, MarkerSeverity, IRelatedInformation } from 'vs/platform/markers/common/markers'; import { MarkersModel, Marker, ResourceMarkers, RelatedInformation } from 'vs/workbench/contrib/markers/browser/markersModel'; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts index b043f0840a1..279baa80c14 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts @@ -126,7 +126,7 @@ export class TempFileMergeEditorModeFactory implements IMergeEditorInputModelFac class TempFileMergeEditorInputModel extends EditorModel implements IMergeEditorInputModel { private readonly savedAltVersionId = observableValue(this, this.model.resultTextModel.getAlternativeVersionId()); - private readonly altVersionId = observableFromEvent( + private readonly altVersionId = observableFromEvent(this, e => this.model.resultTextModel.onDidChangeContent(e), () => /** @description getAlternativeVersionId */ this.model.resultTextModel.getAlternativeVersionId() @@ -340,7 +340,7 @@ export class WorkspaceMergeEditorModeFactory implements IMergeEditorInputModelFa } class WorkspaceMergeEditorInputModel extends EditorModel implements IMergeEditorInputModel { - public readonly isDirty = observableFromEvent( + public readonly isDirty = observableFromEvent(this, Event.any(this.resultTextFileModel.onDidChangeDirty, this.resultTextFileModel.onDidSaveError), () => /** @description isDirty */ this.resultTextFileModel.isDirty() ); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/colors.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/colors.ts index cc6e5700812..f8d1d8b6fcd 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/colors.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/colors.ts @@ -8,7 +8,7 @@ import { mergeCurrentHeaderBackground, mergeIncomingHeaderBackground, registerCo export const diff = registerColor( 'mergeEditor.change.background', - { dark: '#9bb95533', light: '#9bb95533', hcDark: '#9bb95533', hcLight: '#9bb95533', }, + '#9bb95533', localize('mergeEditor.change.background', 'The background color for changes.') ); @@ -38,49 +38,49 @@ export const conflictBorderUnhandledUnfocused = registerColor( export const conflictBorderUnhandledFocused = registerColor( 'mergeEditor.conflict.unhandledFocused.border', - { dark: '#ffa600', light: '#ffa600', hcDark: '#ffa600', hcLight: '#ffa600', }, + '#ffa600', localize('mergeEditor.conflict.unhandledFocused.border', 'The border color of unhandled focused conflicts.') ); export const conflictBorderHandledUnfocused = registerColor( 'mergeEditor.conflict.handledUnfocused.border', - { dark: '#86868649', light: '#86868649', hcDark: '#86868649', hcLight: '#86868649', }, + '#86868649', localize('mergeEditor.conflict.handledUnfocused.border', 'The border color of handled unfocused conflicts.') ); export const conflictBorderHandledFocused = registerColor( 'mergeEditor.conflict.handledFocused.border', - { dark: '#c1c1c1cc', light: '#c1c1c1cc', hcDark: '#c1c1c1cc', hcLight: '#c1c1c1cc', }, + '#c1c1c1cc', localize('mergeEditor.conflict.handledFocused.border', 'The border color of handled focused conflicts.') ); export const handledConflictMinimapOverViewRulerColor = registerColor( 'mergeEditor.conflict.handled.minimapOverViewRuler', - { dark: '#adaca8ee', light: '#adaca8ee', hcDark: '#adaca8ee', hcLight: '#adaca8ee', }, + '#adaca8ee', localize('mergeEditor.conflict.handled.minimapOverViewRuler', 'The foreground color for changes in input 1.') ); export const unhandledConflictMinimapOverViewRulerColor = registerColor( 'mergeEditor.conflict.unhandled.minimapOverViewRuler', - { dark: '#fcba03FF', light: '#fcba03FF', hcDark: '#fcba03FF', hcLight: '#fcba03FF', }, + '#fcba03FF', localize('mergeEditor.conflict.unhandled.minimapOverViewRuler', 'The foreground color for changes in input 1.') ); export const conflictingLinesBackground = registerColor( 'mergeEditor.conflictingLines.background', - { dark: '#ffea0047', light: '#ffea0047', hcDark: '#ffea0047', hcLight: '#ffea0047', }, + '#ffea0047', localize('mergeEditor.conflictingLines.background', 'The background of the "Conflicting Lines" text.') ); const contentTransparency = 0.4; export const conflictInput1Background = registerColor( 'mergeEditor.conflict.input1.background', - { dark: transparent(mergeCurrentHeaderBackground, contentTransparency), light: transparent(mergeCurrentHeaderBackground, contentTransparency), hcDark: transparent(mergeCurrentHeaderBackground, contentTransparency), hcLight: transparent(mergeCurrentHeaderBackground, contentTransparency) }, + transparent(mergeCurrentHeaderBackground, contentTransparency), localize('mergeEditor.conflict.input1.background', 'The background color of decorations in input 1.') ); export const conflictInput2Background = registerColor( 'mergeEditor.conflict.input2.background', - { dark: transparent(mergeIncomingHeaderBackground, contentTransparency), light: transparent(mergeIncomingHeaderBackground, contentTransparency), hcDark: transparent(mergeIncomingHeaderBackground, contentTransparency), hcLight: transparent(mergeIncomingHeaderBackground, contentTransparency) }, + transparent(mergeIncomingHeaderBackground, contentTransparency), localize('mergeEditor.conflict.input2.background', 'The background color of decorations in input 2.') ); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts index b751b6bc0ad..7d564cb4646 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts @@ -10,12 +10,12 @@ import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditor import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; export class EditorGutter extends Disposable { - private readonly scrollTop = observableFromEvent( + private readonly scrollTop = observableFromEvent(this, this._editor.onDidScrollChange, (e) => /** @description editor.onDidScrollChange */ this._editor.getScrollTop() ); private readonly isScrollTopZero = this.scrollTop.map((scrollTop) => /** @description isScrollTopZero */ scrollTop === 0); - private readonly modelAttached = observableFromEvent( + private readonly modelAttached = observableFromEvent(this, this._editor.onDidChangeModel, (e) => /** @description editor.onDidChangeModel */ this._editor.hasModel() ); @@ -126,7 +126,7 @@ export class EditorGutter extends D for (const id of unusedIds) { const view = this.views.get(id)!; view.gutterItemView.dispose(); - this._domNode.removeChild(view.domNode); + view.domNode.remove(); this.views.delete(id); } } @@ -154,4 +154,3 @@ export interface IGutterItemView extends IDisposable update(item: T): void; layout(top: number, height: number, viewTop: number, viewHeight: number): void; } - diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts index 29af08fbafe..b75ca359a2d 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -79,17 +79,17 @@ export abstract class CodeEditorView extends Disposable { this.editor.updateOptions(newOptions); } - public readonly isFocused = observableFromEvent( + public readonly isFocused = observableFromEvent(this, Event.any(this.editor.onDidBlurEditorWidget, this.editor.onDidFocusEditorWidget), () => /** @description editor.hasWidgetFocus */ this.editor.hasWidgetFocus() ); - public readonly cursorPosition = observableFromEvent( + public readonly cursorPosition = observableFromEvent(this, this.editor.onDidChangeCursorPosition, () => /** @description editor.getPosition */ this.editor.getPosition() ); - public readonly selection = observableFromEvent( + public readonly selection = observableFromEvent(this, this.editor.onDidChangeCursorSelection, () => /** @description editor.getSelections */ this.editor.getSelections() ); diff --git a/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts b/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts index 85c475e3c2f..135d0d9a7ee 100644 --- a/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts +++ b/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts b/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts index 0441e4483d3..a07bc71e675 100644 --- a/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts +++ b/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IReader, transaction } from 'vs/base/common/observable'; import { isDefined } from 'vs/base/common/types'; diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts index a53d4d6c346..be0e22ab323 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts @@ -61,11 +61,11 @@ export class ScmMultiDiffSourceResolver implements IMultiDiffSourceResolver { async resolveDiffSource(uri: URI): Promise { const { repositoryUri, groupId } = ScmMultiDiffSourceResolver.parseUri(uri)!; - const repository = await waitForState(observableFromEvent( + const repository = await waitForState(observableFromEvent(this, this._scmService.onDidAddRepository, () => [...this._scmService.repositories].find(r => r.provider.rootUri?.toString() === repositoryUri.toString())) ); - const group = await waitForState(observableFromEvent( + const group = await waitForState(observableFromEvent(this, repository.provider.onDidChangeResourceGroups, () => repository.provider.groups.find(g => g.id === groupId) )); 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 ab728db53f2..721b711f5d2 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts @@ -161,13 +161,13 @@ class ExecutionStateCellStatusBarItem extends Disposable { const state = runState?.state; const { lastRunSuccess } = internalMetadata; if (!state && lastRunSuccess) { - return [{ + return [{ text: `$(${successStateIcon.id})`, color: themeColorFromId(cellStatusIconSuccess), tooltip: localize('notebook.cell.status.success', "Success"), alignment: CellStatusbarAlignment.Left, priority: Number.MAX_SAFE_INTEGER - }]; + } satisfies INotebookCellStatusBarItem]; } else if (!state && lastRunSuccess === false) { return [{ text: `$(${errorStateIcon.id})`, @@ -177,22 +177,22 @@ class ExecutionStateCellStatusBarItem extends Disposable { priority: Number.MAX_SAFE_INTEGER }]; } else if (state === NotebookCellExecutionState.Pending || state === NotebookCellExecutionState.Unconfirmed) { - return [{ + return [{ text: `$(${pendingStateIcon.id})`, tooltip: localize('notebook.cell.status.pending', "Pending"), alignment: CellStatusbarAlignment.Left, priority: Number.MAX_SAFE_INTEGER - }]; + } satisfies INotebookCellStatusBarItem]; } else if (state === NotebookCellExecutionState.Executing) { const icon = runState?.didPause ? executingStateIcon : ThemeIcon.modify(executingStateIcon, 'spin'); - return [{ + return [{ text: `$(${icon.id})`, tooltip: localize('notebook.cell.status.executing', "Executing"), alignment: CellStatusbarAlignment.Left, priority: Number.MAX_SAFE_INTEGER - }]; + } satisfies INotebookCellStatusBarItem]; } return []; @@ -318,12 +318,12 @@ class TimerCellStatusBarItem extends Disposable { } - return { + return { text: formatCellDuration(duration, false), alignment: CellStatusbarAlignment.Left, priority: Number.MAX_SAFE_INTEGER - 5, tooltip - }; + } satisfies INotebookCellStatusBarItem; } override dispose() { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts index eeef10e64d2..205bce806e1 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts @@ -8,6 +8,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -32,7 +33,8 @@ export class EmptyCellEditorHintContribution extends EmptyTextEditorHintContribu @IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService, @IChatAgentService chatAgentService: IChatAgentService, @ITelemetryService telemetryService: ITelemetryService, - @IProductService productService: IProductService + @IProductService productService: IProductService, + @IContextMenuService contextMenuService: IContextMenuService ) { super( editor, @@ -44,7 +46,8 @@ export class EmptyCellEditorHintContribution extends EmptyTextEditorHintContribu inlineChatSessionService, chatAgentService, telemetryService, - productService + productService, + contextMenuService ); const activeEditor = getNotebookEditorFromEditorPane(this._editorService.activeEditorPane); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts index 5ad7c674268..197230a61cc 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts @@ -10,8 +10,7 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { CENTER_ACTIVE_CELL } from 'vs/workbench/contrib/notebook/browser/contrib/navigation/arrow'; import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { SELECT_NOTEBOOK_INDENTATION_ID } from 'vs/workbench/contrib/notebook/browser/controller/editActions'; @@ -20,8 +19,9 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; class ImplictKernelSelector implements IDisposable { @@ -179,8 +179,6 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { } } -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(KernelStatus, LifecyclePhase.Restored); - export class ActiveCellStatus extends Disposable implements IWorkbenchContribution { private readonly _itemDisposables = this._register(new DisposableStore()); @@ -255,9 +253,7 @@ export class ActiveCellStatus extends Disposable implements IWorkbenchContributi } } -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ActiveCellStatus, LifecyclePhase.Restored); - -export class NotebookIndentationStatus extends Disposable implements IWorkbenchContribution { +export class NotebookIndentationStatus extends Disposable { private readonly _itemDisposables = this._register(new DisposableStore()); private readonly _accessor = this._register(new MutableDisposable()); @@ -339,4 +335,33 @@ export class NotebookIndentationStatus extends Disposable implements IWorkbenchC } } -registerWorkbenchContribution2(NotebookIndentationStatus.ID, NotebookIndentationStatus, WorkbenchPhase.AfterRestored); // TODO@Yoyokrazy -- unsure on the phase +export class NotebookEditorStatusContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'notebook.contrib.editorStatus'; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorService editorService: IEditorService + ) { + super(); + + // Main Editor Status + const mainInstantiationService = instantiationService.createChild(new ServiceCollection( + [IEditorService, editorService.createScoped('main', this._store)] + )); + this._register(mainInstantiationService.createInstance(KernelStatus)); + this._register(mainInstantiationService.createInstance(ActiveCellStatus)); + this._register(mainInstantiationService.createInstance(NotebookIndentationStatus)); + + // Auxiliary Editor Status + this._register(editorGroupService.onDidCreateAuxiliaryEditorPart(({ part, instantiationService, disposables }) => { + disposables.add(instantiationService.createInstance(KernelStatus)); + disposables.add(instantiationService.createInstance(ActiveCellStatus)); + disposables.add(instantiationService.createInstance(NotebookIndentationStatus)); + })); + } +} + + +registerWorkbenchContribution2(NotebookEditorStatusContribution.ID, NotebookEditorStatusContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts index 0901d295edd..cf98120913c 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { Event, Emitter } from 'vs/base/common/event'; -import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { INotebookFindScope, NotebookFindScopeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export interface INotebookFindChangeEvent { markupInput?: boolean; markupPreview?: boolean; codeInput?: boolean; codeOutput?: boolean; - searchInRanges?: boolean; + findScope?: boolean; } export class NotebookFindFilters extends Disposable { @@ -70,31 +70,19 @@ export class NotebookFindFilters extends Disposable { } } - private _searchInRanges: boolean = false; + private _findScope: INotebookFindScope = { findScopeType: NotebookFindScopeType.None }; - get searchInRanges(): boolean { - return this._searchInRanges; + get findScope(): INotebookFindScope { + return this._findScope; } - set searchInRanges(value: boolean) { - if (this._searchInRanges !== value) { - this._searchInRanges = value; - this._onDidChange.fire({ searchInRanges: value }); + set findScope(value: INotebookFindScope) { + if (this._findScope !== value) { + this._findScope = value; + this._onDidChange.fire({ findScope: true }); } } - private _selectedRanges: ICellRange[] = []; - - get selectedRanges(): ICellRange[] { - return this._selectedRanges; - } - - set selectedRanges(value: ICellRange[]) { - if (this._selectedRanges !== value) { - this._selectedRanges = value; - this._onDidChange.fire({ searchInRanges: this._searchInRanges }); - } - } private readonly _initialMarkupInput: boolean; private readonly _initialMarkupPreview: boolean; @@ -106,8 +94,7 @@ export class NotebookFindFilters extends Disposable { markupPreview: boolean, codeInput: boolean, codeOutput: boolean, - searchInRanges: boolean, - selectedRanges: ICellRange[] + findScope: INotebookFindScope ) { super(); @@ -115,8 +102,7 @@ export class NotebookFindFilters extends Disposable { this._markupPreview = markupPreview; this._codeInput = codeInput; this._codeOutput = codeOutput; - this._searchInRanges = searchInRanges; - this._selectedRanges = selectedRanges; + this._findScope = findScope; this._initialMarkupInput = markupInput; this._initialMarkupPreview = markupPreview; @@ -125,7 +111,7 @@ export class NotebookFindFilters extends Disposable { } isModified(): boolean { - // do not include searchInRanges or selectedRanges in the check. This will incorrectly mark the filter icon as modified + // do not include findInSelection or either selectedRanges in the check. This will incorrectly mark the filter icon as modified return ( this._markupInput !== this._initialMarkupInput || this._markupPreview !== this._initialMarkupPreview @@ -139,7 +125,6 @@ export class NotebookFindFilters extends Disposable { this._markupPreview = v.markupPreview; this._codeInput = v.codeInput; this._codeOutput = v.codeOutput; - this._searchInRanges = v.searchInRanges; - this._selectedRanges = v.selectedRanges; + this._findScope = v.findScope; } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel.ts index 6fa5a4fea6c..214de0211a6 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel.ts @@ -57,7 +57,6 @@ export class FindMatchDecorationModel extends Disposable { }); this._currentMatchCellDecorations = this._notebookEditor.deltaCellDecorations(this._currentMatchCellDecorations, [{ - ownerId: cell.handle, handle: cell.handle, options: { overviewRuler: { @@ -67,7 +66,7 @@ export class FindMatchDecorationModel extends Disposable { position: NotebookOverviewRulerLane.Center } } - } as INotebookDeltaDecoration]); + }]); return null; } @@ -80,7 +79,6 @@ export class FindMatchDecorationModel extends Disposable { this._currentMatchDecorations = { kind: 'output', index: index }; this._currentMatchCellDecorations = this._notebookEditor.deltaCellDecorations(this._currentMatchCellDecorations, [{ - ownerId: cell.handle, handle: cell.handle, options: { overviewRuler: { @@ -90,7 +88,7 @@ export class FindMatchDecorationModel extends Disposable { position: NotebookOverviewRulerLane.Center } } - } as INotebookDeltaDecoration]); + } satisfies INotebookDeltaDecoration]); return offset; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts index de522b344a5..0ca2d422f53 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -3,21 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { findFirstIdxMonotonousOrArrLen } from 'vs/base/common/arraysFind'; import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async'; -import { INotebookEditor, CellEditState, CellFindMatchWithIndex, CellWebviewFindMatch, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; import { FindMatch } from 'vs/editor/common/model'; import { PrefixSumComputer } from 'vs/editor/common/model/prefixSumComputer'; import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/browser/findState'; -import { CellKind, INotebookSearchOptions, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { findFirstIdxMonotonousOrArrLen } from 'vs/base/common/arraysFind'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; import { FindMatchDecorationModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel'; +import { CellEditState, CellFindMatchWithIndex, CellWebviewFindMatch, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { CellKind, INotebookFindOptions, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class CellFindMatchModel implements CellFindMatchWithIndex { readonly cell: ICellViewModel; @@ -115,7 +115,7 @@ export class FindModel extends Disposable { } private _updateCellStates(e: FindReplaceStateChangedEvent) { - if (!this._state.filters?.markupInput || !this._state.filters?.markupPreview || !this._state.filters?.searchInRanges || !this._state.filters?.selectedRanges) { + if (!this._state.filters?.markupInput || !this._state.filters?.markupPreview || !this._state.filters?.findScope) { return; } @@ -127,7 +127,7 @@ export class FindModel extends Disposable { } // search markup sources first to decide if a markup cell should be in editing mode const wordSeparators = this._configurationService.inspect('editor.wordSeparators').value; - const options: INotebookSearchOptions = { + const options: INotebookFindOptions = { regex: this._state.isRegex, wholeWord: this._state.wholeWord, caseSensitive: this._state.matchCase, @@ -136,8 +136,7 @@ export class FindModel extends Disposable { includeCodeInput: false, includeMarkupPreview: false, includeOutput: false, - searchInRanges: this._state.filters?.searchInRanges, - selectedRanges: this._state.filters?.selectedRanges + findScope: this._state.filters?.findScope, }; const contentMatches = viewModel.find(this._state.searchString, options); @@ -476,7 +475,7 @@ export class FindModel extends Disposable { const val = this._state.searchString; const wordSeparators = this._configurationService.inspect('editor.wordSeparators').value; - const options: INotebookSearchOptions = { + const options: INotebookFindOptions = { regex: this._state.isRegex, wholeWord: this._state.wholeWord, caseSensitive: this._state.matchCase, @@ -485,8 +484,7 @@ export class FindModel extends Disposable { includeCodeInput: this._state.filters?.codeInput ?? true, includeMarkupPreview: !!this._state.filters?.markupPreview, includeOutput: !!this._state.filters?.codeOutput, - searchInRanges: this._state.filters?.searchInRanges, - selectedRanges: this._state.filters?.selectedRanges + findScope: this._state.filters?.findScope, }; ret = await this._notebookEditor.find(val, options, token); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/media/notebookFind.css b/src/vs/workbench/contrib/notebook/browser/contrib/find/media/notebookFind.css index fe115e23081..d61cc797497 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/media/notebookFind.css +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/media/notebookFind.css @@ -19,3 +19,7 @@ padding: 0 2px; box-sizing: border-box; } + +.monaco-workbench .nb-findScope { + background-color: var(--vscode-editor-findRangeHighlightBackground); +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts index fd39a06a3f7..fb3d93d9d5e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts @@ -10,6 +10,7 @@ import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; import { FindStartFocusAction, getSelectionSearchString, IFindStartOptions, StartFindAction, StartFindReplaceAction } from 'vs/editor/contrib/find/browser/findController'; @@ -19,13 +20,12 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IShowNotebookFindWidgetOptions, NotebookFindContrib } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; +import { INotebookCommandContext, NotebookMultiCellAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; -import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, NotebookFindScopeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INTERACTIVE_WINDOW_IS_ACTIVE_EDITOR, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { INotebookCommandContext, NotebookMultiCellAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; registerNotebookContribution(NotebookFindContrib.id, NotebookFindContrib); @@ -78,12 +78,7 @@ registerAction2(class extends NotebookMultiCellAction { } const controller = editor.getContribution(NotebookFindContrib.id); - - if (context.selectedCells.length > 1) { - controller.show(undefined, { searchInRanges: true, selectedRanges: editor.getSelections() }); - } else { - controller.show(undefined, { searchInRanges: false, selectedRanges: [] }); - } + controller.show(undefined, { findScope: { findScopeType: NotebookFindScopeType.None } }); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index 7b261f5dc0e..8c4b59a688e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -3,55 +3,56 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; +import 'vs/css!./notebookFindReplaceWidget'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput'; import { ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput'; import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; +import { ISashEvent, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; +import { IToggleStyles, Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Widget } from 'vs/base/browser/ui/widget'; +import { Action, ActionRunner, IAction, IActionRunner, Separator } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; +import { Codicon } from 'vs/base/common/codicons'; import { KeyCode } from 'vs/base/common/keyCodes'; -import 'vs/css!./notebookFindReplaceWidget'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { isSafari } from 'vs/base/common/platform'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { Range } from 'vs/editor/common/core/range'; import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/browser/findState'; import { findNextMatchIcon, findPreviousMatchIcon, findReplaceAllIcon, findReplaceIcon, findSelectionIcon, SimpleButton } from 'vs/editor/contrib/find/browser/findWidget'; -import * as nls from 'vs/nls'; -import { ContextScopedReplaceInput, registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; +import { parseReplaceString, ReplacePattern } from 'vs/editor/contrib/find/browser/replacePattern'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { ContextScopedReplaceInput, registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { defaultInputBoxStyles, defaultProgressBarStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { asCssVariable, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerIcon, widgetClose } from 'vs/platform/theme/common/iconRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { parseReplaceString, ReplacePattern } from 'vs/editor/contrib/find/browser/replacePattern'; -import { Codicon } from 'vs/base/common/codicons'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { Action, ActionRunner, IAction, IActionRunner, Separator } from 'vs/base/common/actions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IMenu } from 'vs/platform/actions/common/actions'; -import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { filterIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; -import { isSafari } from 'vs/base/common/platform'; -import { ISashEvent, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; -import { INotebookDeltaDecoration, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { defaultInputBoxStyles, defaultProgressBarStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { IToggleStyles, Toggle } from 'vs/base/browser/ui/toggle/toggle'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { asCssVariable, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IShowNotebookFindWidgetOptions } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; +import { ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, INotebookDeltaDecoration, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookFindScopeType, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous Match"); -// const NLS_FILTER_BTN_LABEL = nls.localize('label.findFilterButton', "Search in View"); const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next Match"); -const NLS_FIND_IN_CELL_SELECTION_BTN_LABEL = nls.localize('label.findInCellSelectionButton', "Find in Cell Selection"); +const NLS_TOGGLE_SELECTION_FIND_TITLE = nls.localize('label.toggleSelectionFind', "Find in Selection"); const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close"); const NLS_TOGGLE_REPLACE_MODE_BTN_LABEL = nls.localize('label.toggleReplaceButton', "Toggle Replace"); const NLS_REPLACE_INPUT_LABEL = nls.localize('label.replace', "Replace"); @@ -66,7 +67,7 @@ const NOTEBOOK_FIND_IN_MARKUP_PREVIEW = nls.localize('notebook.find.filter.findI const NOTEBOOK_FIND_IN_CODE_INPUT = nls.localize('notebook.find.filter.findInCodeInput', "Code Cell Source"); const NOTEBOOK_FIND_IN_CODE_OUTPUT = nls.localize('notebook.find.filter.findInCodeOutput', "Code Cell Output"); -const NOTEBOOK_FIND_WIDGET_INITIAL_WIDTH = 318; +const NOTEBOOK_FIND_WIDGET_INITIAL_WIDTH = 419; const NOTEBOOK_FIND_WIDGET_INITIAL_HORIZONTAL_PADDING = 4; class NotebookFindFilterActionViewItem extends DropdownMenuActionViewItem { constructor(readonly filters: NotebookFindFilters, action: IAction, options: IActionViewItemOptions, actionRunner: IActionRunner, @IContextMenuService contextMenuService: IContextMenuService) { @@ -319,8 +320,8 @@ export abstract class SimpleFindReplaceWidget extends Widget { private _filters: NotebookFindFilters; private readonly inSelectionToggle: Toggle; - private searchInSelectionEnabled: boolean; - private selectionDecorationIds: string[] = []; + private cellSelectionDecorationIds: string[] = []; + private textSelectionDecorationIds: ICellModelDecorations[] = []; constructor( @IContextViewService private readonly _contextViewService: IContextViewService, @@ -341,7 +342,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { codeOutput: boolean; }>(NotebookSetting.findFilters) ?? { markupSource: true, markupPreview: true, codeSource: true, codeOutput: true }; - this._filters = new NotebookFindFilters(findFilters.markupSource, findFilters.markupPreview, findFilters.codeSource, findFilters.codeOutput, false, []); + this._filters = new NotebookFindFilters(findFilters.markupSource, findFilters.markupPreview, findFilters.codeSource, findFilters.codeOutput, { findScopeType: NotebookFindScopeType.None }); this._state.change({ filters: this._filters }, false); this._filters.onDidChange(() => { @@ -386,6 +387,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { null, this._contextViewService, { + // width:FIND_INPUT_AREA_WIDTH, label: NLS_FIND_INPUT_LABEL, placeholder: NLS_FIND_INPUT_PLACEHOLDER, validation: (value: string): InputBoxMessage | null => { @@ -462,22 +464,54 @@ export abstract class SimpleFindReplaceWidget extends Widget { this.inSelectionToggle = this._register(new Toggle({ icon: findSelectionIcon, - title: NLS_FIND_IN_CELL_SELECTION_BTN_LABEL, + title: NLS_TOGGLE_SELECTION_FIND_TITLE, isChecked: false, inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground), inputActiveOptionBorder: asCssVariable(inputActiveOptionBorder), inputActiveOptionForeground: asCssVariable(inputActiveOptionForeground), })); + this.inSelectionToggle.domNode.style.display = 'inline'; this.inSelectionToggle.onChange(() => { const checked = this.inSelectionToggle.checked; - this._filters.searchInRanges = checked; if (checked) { - this._filters.selectedRanges = this._notebookEditor.getSelections(); - this.setCellSelectionDecorations(); + // selection logic: + // 1. if there are multiple cells, do that. + // 2. if there is only one cell, do the following: + // - if there is a multi-line range highlighted, textual in selection + // - if there is no range, cell in selection for that cell + + const cellSelection: ICellRange[] = this._notebookEditor.getSelections(); + const textSelection: Range[] = this._notebookEditor.getSelectionViewModels()[0].getSelections(); + + if (cellSelection.length > 1 || cellSelection.some(range => range.end - range.start > 1)) { + this._filters.findScope = { + findScopeType: NotebookFindScopeType.Cells, + selectedCellRanges: cellSelection + }; + this.setCellSelectionDecorations(); + + } else if (textSelection.length > 1 || textSelection.some(range => range.endLineNumber - range.startLineNumber >= 1)) { + this._filters.findScope = { + findScopeType: NotebookFindScopeType.Text, + selectedCellRanges: cellSelection, + selectedTextRanges: textSelection + }; + this.setTextSelectionDecorations(textSelection, this._notebookEditor.getSelectionViewModels()[0]); + + } else { + this._filters.findScope = { + findScopeType: NotebookFindScopeType.Cells, + selectedCellRanges: cellSelection + }; + this.setCellSelectionDecorations(); + } } else { - this._filters.selectedRanges = []; + this._filters.findScope = { + findScopeType: NotebookFindScopeType.None + }; this.clearCellSelectionDecorations(); + this.clearTextSelectionDecorations(); } }); @@ -496,22 +530,6 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._innerFindDomNode.appendChild(this.inSelectionToggle.domNode); this._innerFindDomNode.appendChild(closeBtn.domNode); - this.searchInSelectionEnabled = this._configurationService.getValue(NotebookSetting.findScope); - this.inSelectionToggle.domNode.style.display = this.searchInSelectionEnabled ? 'inline' : 'none'; - - this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(NotebookSetting.findScope)) { - this.searchInSelectionEnabled = this._configurationService.getValue(NotebookSetting.findScope); - if (this.searchInSelectionEnabled) { - this.inSelectionToggle.domNode.style.display = 'inline'; - } else { - this.inSelectionToggle.domNode.style.display = 'none'; - this.inSelectionToggle.checked = false; - this.clearCellSelectionDecorations(); - } - } - }); - // _domNode wraps _innerDomNode, ensuring that this._domNode.appendChild(this._innerFindDomNode); @@ -704,11 +722,37 @@ export abstract class SimpleFindReplaceWidget extends Widget { options: { className: 'nb-multiCellHighlight', outputClassName: 'nb-multiCellHighlight' } } satisfies INotebookDeltaDecoration); } - this.selectionDecorationIds = this._notebookEditor.deltaCellDecorations([], decorations); + this.cellSelectionDecorationIds = this._notebookEditor.deltaCellDecorations([], decorations); } private clearCellSelectionDecorations() { - this._notebookEditor.deltaCellDecorations(this.selectionDecorationIds, []); + this._notebookEditor.deltaCellDecorations(this.cellSelectionDecorationIds, []); + } + + private setTextSelectionDecorations(textRanges: Range[], cell: ICellViewModel) { + this._notebookEditor.changeModelDecorations(changeAccessor => { + const decorations: ICellModelDeltaDecorations[] = []; + for (const range of textRanges) { + decorations.push({ + ownerId: cell.handle, + decorations: [{ + range: range, + options: { + description: 'text search range for notebook search scope', + isWholeLine: true, + className: 'nb-findScope' + } + }] + }); + } + this.textSelectionDecorationIds = changeAccessor.deltaDecorations([], decorations); + }); + } + + private clearTextSelectionDecorations() { + this._notebookEditor.changeModelDecorations(changeAccessor => { + changeAccessor.deltaDecorations(this.textSelectionDecorationIds, []); + }); } protected _updateMatchesCount(): void { @@ -717,9 +761,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { override dispose() { super.dispose(); - if (this._domNode && this._domNode.parentElement) { - this._domNode.parentElement.removeChild(this._domNode); - } + this._domNode.remove(); } public getDomNode() { @@ -750,20 +792,11 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._findInput.focus(); } - public show(initialInput?: string, options?: { focus?: boolean; searchInRanges?: boolean; selectedRanges?: ICellRange[] }): void { + public show(initialInput?: string, options?: IShowNotebookFindWidgetOptions): void { if (initialInput) { this._findInput.setValue(initialInput); } - if (this.searchInSelectionEnabled && options?.searchInRanges !== undefined) { - this._filters.searchInRanges = options.searchInRanges; - this.inSelectionToggle.checked = options.searchInRanges; - if (options.searchInRanges && options.selectedRanges) { - this._filters.selectedRanges = options.selectedRanges; - this.setCellSelectionDecorations(); - } - } - this._isVisible = true; setTimeout(() => { @@ -812,7 +845,10 @@ export abstract class SimpleFindReplaceWidget extends Widget { public hide(): void { if (this._isVisible) { this.inSelectionToggle.checked = false; - this._notebookEditor.deltaCellDecorations(this.selectionDecorationIds, []); + this._notebookEditor.deltaCellDecorations(this.cellSelectionDecorationIds, []); + this._notebookEditor.changeModelDecorations(changeAccessor => { + changeAccessor.deltaDecorations(this.textSelectionDecorationIds, []); + }); this._domNode.classList.remove('visible-transition'); this._domNode.setAttribute('aria-hidden', 'true'); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts index c4307db60b9..15954bc2b81 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts @@ -25,8 +25,8 @@ import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contr import { FindModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; import { SimpleFindReplaceWidget } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget'; import { CellEditState, ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookFindScope } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; const FIND_HIDE_TRANSITION = 'find-hide-transition'; const FIND_SHOW_TRANSITION = 'find-show-transition'; @@ -40,8 +40,7 @@ export interface IShowNotebookFindWidgetOptions { matchIndex?: number; focus?: boolean; searchStringSeededFrom?: { cell: ICellViewModel; range: Range }; - searchInRanges?: boolean; - selectedRanges?: ICellRange[]; + findScope?: INotebookFindScope; } export class NotebookFindContrib extends Disposable implements INotebookEditorContribution { @@ -348,9 +347,7 @@ class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEdi this._matchesCount.title = ''; // remove previous content - if (this._matchesCount.firstChild) { - this._matchesCount.removeChild(this._matchesCount.firstChild); - } + this._matchesCount.firstChild?.remove(); let label: string; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts index e6656eaac73..203839dfd85 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts @@ -22,7 +22,7 @@ import { CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION } from 'vs/workbench/contrib/not import { INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, findTargetCellEditor } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CURSOR_NAVIGATION_MODE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_INPUT_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CURSOR_NAVIGATION_MODE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_INPUT_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_CELL_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; const NOTEBOOK_FOCUS_TOP = 'notebook.focusTop'; const NOTEBOOK_FOCUS_BOTTOM = 'notebook.focusBottom'; @@ -104,10 +104,10 @@ registerAction2(class FocusNextCellAction extends NotebookCellAction { weight: KeybindingWeight.WorkbenchContrib }, { - when: NOTEBOOK_EDITOR_FOCUSED, - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, }, - weight: KeybindingWeight.WorkbenchContrib + when: ContextKeyExpr.and(NOTEBOOK_CELL_EDITOR_FOCUSED, CONTEXT_ACCESSIBILITY_MODE_ENABLED), + primary: KeyMod.CtrlCmd | KeyCode.PageDown, + mac: { primary: KeyMod.WinCtrl | KeyCode.PageUp, }, + weight: KeybindingWeight.WorkbenchContrib + 1 }, ] }); @@ -180,10 +180,10 @@ registerAction2(class FocusPreviousCellAction extends NotebookCellAction { weight: KeybindingWeight.WorkbenchContrib, // markdown keybinding, focus on list: higher weight to override list.focusDown }, { - when: NOTEBOOK_EDITOR_FOCUSED, - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp }, - weight: KeybindingWeight.WorkbenchContrib + when: ContextKeyExpr.and(NOTEBOOK_CELL_EDITOR_FOCUSED, CONTEXT_ACCESSIBILITY_MODE_ENABLED), + primary: KeyMod.CtrlCmd | KeyCode.PageUp, + mac: { primary: KeyMod.WinCtrl | KeyCode.PageUp, }, + weight: KeybindingWeight.WorkbenchContrib + 1 }, ], }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index 1a0f19d4a58..c9d3c97a772 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -29,8 +29,8 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { IEditorPane } from 'vs/workbench/common/editor'; import { CellFoldingState, CellRevealType, ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, INotebookEditor, INotebookEditorOptions, INotebookEditorPane, INotebookViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; -import { NotebookCellOutlineProvider } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; -import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookCellOutlineDataSource, NotebookCellOutlineDataSource } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource'; +import { CellKind, NotebookCellsChangeType, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IBreadcrumbsDataSource, IOutline, IOutlineComparator, IOutlineCreator, IOutlineListConfig, IOutlineService, IQuickPickDataSource, IQuickPickOutlineElement, OutlineChangeEvent, OutlineConfigCollapseItemsValues, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; @@ -46,12 +46,14 @@ import { MenuEntryActionViewItem, createAndFillInActionBarActions } from 'vs/pla import { IAction } from 'vs/base/common/actions'; import { NotebookSectionArgs } from 'vs/workbench/contrib/notebook/browser/controller/sectionActions'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; -import { disposableTimeout } from 'vs/base/common/async'; +import { Delayer, disposableTimeout } from 'vs/base/common/async'; import { IOutlinePane } from 'vs/workbench/contrib/outline/browser/outline'; import { Codicon } from 'vs/base/common/codicons'; import { NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { NotebookOutlineConstants } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory'; -import { INotebookCellOutlineProviderFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory'; +import { INotebookCellOutlineDataSourceFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory'; +import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; class NotebookOutlineTemplate { @@ -286,33 +288,44 @@ class NotebookOutlineVirtualDelegate implements IListVirtualDelegate { + private readonly _disposables = new DisposableStore(); + + private gotoShowCodeCellSymbols: boolean; + constructor( - private _getEntries: () => OutlineEntry[], + private readonly notebookCellOutlineDataSourceRef: IReference | undefined, @IConfigurationService private readonly _configurationService: IConfigurationService, @IThemeService private readonly _themeService: IThemeService - ) { } + ) { + this.gotoShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); + + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(NotebookSetting.gotoSymbolsAllSymbols)) { + this.gotoShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); + } + })); + } getQuickPickElements(): IQuickPickOutlineElement[] { const bucket: OutlineEntry[] = []; - for (const entry of this._getEntries()) { + for (const entry of this.notebookCellOutlineDataSourceRef?.object?.entries ?? []) { entry.asFlatList(bucket); } const result: IQuickPickOutlineElement[] = []; const { hasFileIcons } = this._themeService.getFileIconTheme(); - const showSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); const isSymbol = (element: OutlineEntry) => !!element.symbolKind; const isCodeCell = (element: OutlineEntry) => (element.cell.cellKind === CellKind.Code && element.level === NotebookOutlineConstants.NonHeaderOutlineLevel); // code cell entries are exactly level 7 by this constant for (let i = 0; i < bucket.length; i++) { const element = bucket[i]; const nextElement = bucket[i + 1]; // can be undefined - if (!showSymbols + if (!this.gotoShowCodeCellSymbols && isSymbol(element)) { continue; } - if (showSymbols + if (this.gotoShowCodeCellSymbols && isCodeCell(element) && nextElement && isSymbol(nextElement)) { continue; @@ -330,32 +343,98 @@ export class NotebookQuickPickProvider implements IQuickPickDataSource { + + private readonly _disposables = new DisposableStore(); + + private showCodeCells: boolean; + private showCodeCellSymbols: boolean; + private showMarkdownHeadersOnly: boolean; + constructor( - private _getEntries: () => OutlineEntry[], + private readonly outlineDataSourceRef: IReference | undefined, @IConfigurationService private readonly _configurationService: IConfigurationService, - ) { } + ) { + this.showCodeCells = this._configurationService.getValue(NotebookSetting.outlineShowCodeCells); + this.showCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); + this.showMarkdownHeadersOnly = this._configurationService.getValue(NotebookSetting.outlineShowMarkdownHeadersOnly); - *getChildren(element: NotebookCellOutline | OutlineEntry): Iterable { - const showCodeCells = this._configurationService.getValue(NotebookSetting.outlineShowCodeCells); - const showCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); - const showMarkdownHeadersOnly = this._configurationService.getValue(NotebookSetting.outlineShowMarkdownHeadersOnly); + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(NotebookSetting.outlineShowCodeCells)) { + this.showCodeCells = this._configurationService.getValue(NotebookSetting.outlineShowCodeCells); + } + if (e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols)) { + this.showCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); + } + if (e.affectsConfiguration(NotebookSetting.outlineShowMarkdownHeadersOnly)) { + this.showMarkdownHeadersOnly = this._configurationService.getValue(NotebookSetting.outlineShowMarkdownHeadersOnly); + } + })); + } + + public getActiveEntry(): OutlineEntry | undefined { + const newActive = this.outlineDataSourceRef?.object?.activeElement; + if (!newActive) { + return undefined; + } + + if (!this.filterEntry(newActive)) { + return newActive; + } + + // find a valid parent + let parent = newActive.parent; + while (parent) { + if (this.filterEntry(parent)) { + parent = parent.parent; + } else { + return parent; + } + } + + // no valid parent found, return undefined + return undefined; + } + + /** + * Checks if the given outline entry should be filtered out of the outlinePane + * + * @param entry the OutlineEntry to check + * @returns true if the entry should be filtered out of the outlinePane + */ + private filterEntry(entry: OutlineEntry): boolean { + // if any are true, return true, this entry should NOT be included in the outline + if ( + (this.showMarkdownHeadersOnly && entry.cell.cellKind === CellKind.Markup && entry.level === NotebookOutlineConstants.NonHeaderOutlineLevel) || // show headers only + cell is mkdn + is level 7 (not header) + (!this.showCodeCells && entry.cell.cellKind === CellKind.Code) || // show code cells off + cell is code + (!this.showCodeCellSymbols && entry.cell.cellKind === CellKind.Code && entry.level > NotebookOutlineConstants.NonHeaderOutlineLevel) // show symbols off + cell is code + is level >7 (nb symbol levels) + ) { + return true; + } + + return false; + } + *getChildren(element: NotebookCellOutline | OutlineEntry): Iterable { const isOutline = element instanceof NotebookCellOutline; - const entries = isOutline ? this._getEntries() : element.children; + const entries = isOutline ? this.outlineDataSourceRef?.object?.entries ?? [] : element.children; for (const entry of entries) { if (entry.cell.cellKind === CellKind.Markup) { - if (!showMarkdownHeadersOnly) { + if (!this.showMarkdownHeadersOnly) { yield entry; } else if (entry.level < NotebookOutlineConstants.NonHeaderOutlineLevel) { yield entry; } - } else if (showCodeCells && entry.cell.cellKind === CellKind.Code) { - if (showCodeCellSymbols) { + } else if (this.showCodeCells && entry.cell.cellKind === CellKind.Code) { + if (this.showCodeCellSymbols) { yield entry; } else if (entry.level === NotebookOutlineConstants.NonHeaderOutlineLevel) { yield entry; @@ -363,26 +442,45 @@ export class NotebookOutlinePaneProvider implements IDataSource { + + private readonly _disposables = new DisposableStore(); + + private showCodeCells: boolean; + constructor( - private _getActiveElement: () => OutlineEntry | undefined, + private readonly outlineDataSourceRef: IReference | undefined, @IConfigurationService private readonly _configurationService: IConfigurationService, - ) { } + ) { + this.showCodeCells = this._configurationService.getValue(NotebookSetting.breadcrumbsShowCodeCells); + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(NotebookSetting.breadcrumbsShowCodeCells)) { + this.showCodeCells = this._configurationService.getValue(NotebookSetting.breadcrumbsShowCodeCells); + } + })); + } getBreadcrumbElements(): readonly OutlineEntry[] { const result: OutlineEntry[] = []; - const showCodeCells = this._configurationService.getValue(NotebookSetting.breadcrumbsShowCodeCells); - let candidate = this._getActiveElement(); + let candidate = this.outlineDataSourceRef?.object?.activeElement; while (candidate) { - if (showCodeCells || candidate.cell.cellKind !== CellKind.Code) { + if (this.showCodeCells || candidate.cell.cellKind !== CellKind.Code) { result.unshift(candidate); } candidate = candidate.parent; } return result; } + + dispose(): void { + this._disposables.dispose(); + } } class NotebookComparator implements IOutlineComparator { @@ -401,64 +499,80 @@ class NotebookComparator implements IOutlineComparator { } export class NotebookCellOutline implements IOutline { + readonly outlineKind = 'notebookCells'; - private readonly _dispoables = new DisposableStore(); + private readonly _disposables = new DisposableStore(); + private readonly _modelDisposables = new DisposableStore(); + private readonly _dataSourceDisposables = new DisposableStore(); private readonly _onDidChange = new Emitter(); - readonly onDidChange: Event = this._onDidChange.event; - get entries(): OutlineEntry[] { - return this._outlineProviderReference?.object?.entries ?? []; - } - - private readonly _entriesDisposables = new DisposableStore(); + private readonly delayerRecomputeState: Delayer = this._disposables.add(new Delayer(300)); + private readonly delayerRecomputeActive: Delayer = this._disposables.add(new Delayer(200)); + // this can be long, because it will force a recompute at the end, so ideally we only do this once all nb language features are registered + private readonly delayerRecomputeSymbols: Delayer = this._disposables.add(new Delayer(2000)); readonly config: IOutlineListConfig; + private _outlineDataSourceReference: IReference | undefined; + // These three fields will always be set via setDataSources() on L475 + private _treeDataSource!: IDataSource; + private _quickPickDataSource!: IQuickPickDataSource; + private _breadcrumbsDataSource!: IBreadcrumbsDataSource; - readonly outlineKind = 'notebookCells'; + // view settings + private gotoShowCodeCellSymbols: boolean; + private outlineShowCodeCellSymbols: boolean; + // getters get activeElement(): OutlineEntry | undefined { - return this._outlineProviderReference?.object?.activeElement; + this.checkDelayer(); + if (this._target === OutlineTarget.OutlinePane) { + return (this.config.treeDataSource as NotebookOutlinePaneProvider).getActiveEntry(); + } else { + console.error('activeElement should not be called outside of the OutlinePane'); + return undefined; + } + } + get entries(): OutlineEntry[] { + this.checkDelayer(); + return this._outlineDataSourceReference?.object?.entries ?? []; + } + get uri(): URI | undefined { + return this._outlineDataSourceReference?.object?.uri; + } + get isEmpty(): boolean { + return this._outlineDataSourceReference?.object?.isEmpty ?? true; } - private _outlineProviderReference: IReference | undefined; - private readonly _localDisposables = new DisposableStore(); + private checkDelayer() { + if (this.delayerRecomputeState.isTriggered()) { + this.delayerRecomputeState.cancel(); + this.recomputeState(); + } + } constructor( private readonly _editor: INotebookEditorPane, - _target: OutlineTarget, - @IInstantiationService instantiationService: IInstantiationService, + private readonly _target: OutlineTarget, + @IThemeService private readonly _themeService: IThemeService, @IEditorService private readonly _editorService: IEditorService, - @IConfigurationService _configurationService: IConfigurationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, ) { - const installSelectionListener = () => { - const notebookEditor = _editor.getControl(); - if (!notebookEditor?.hasModel()) { - this._outlineProviderReference?.dispose(); - this._outlineProviderReference = undefined; - this._localDisposables.clear(); - } else { - this._outlineProviderReference?.dispose(); - this._localDisposables.clear(); - this._outlineProviderReference = instantiationService.invokeFunction((accessor) => accessor.get(INotebookCellOutlineProviderFactory).getOrCreate(notebookEditor, _target)); - this._localDisposables.add(this._outlineProviderReference.object.onDidChange(e => { - this._onDidChange.fire(e); - })); - } - }; + this.gotoShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); + this.outlineShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); - this._dispoables.add(_editor.onDidChangeModel(() => { - installSelectionListener(); - })); + this.initializeOutline(); - installSelectionListener(); const delegate = new NotebookOutlineVirtualDelegate(); - const renderers = [instantiationService.createInstance(NotebookOutlineRenderer, this._editor.getControl(), _target)]; + const renderers = [this._instantiationService.createInstance(NotebookOutlineRenderer, this._editor.getControl(), this._target)]; const comparator = new NotebookComparator(); const options: IWorkbenchDataTreeOptions = { - collapseByDefault: _target === OutlineTarget.Breadcrumbs || (_target === OutlineTarget.OutlinePane && _configurationService.getValue(OutlineConfigKeys.collapseItems) === OutlineConfigCollapseItemsValues.Collapsed), + collapseByDefault: this._target === OutlineTarget.Breadcrumbs || (this._target === OutlineTarget.OutlinePane && this._configurationService.getValue(OutlineConfigKeys.collapseItems) === OutlineConfigCollapseItemsValues.Collapsed), expandOnlyOnTwistieClick: true, multipleSelectionSupport: false, accessibilityProvider: new NotebookOutlineAccessibility(), @@ -467,9 +581,9 @@ export class NotebookCellOutline implements IOutline { }; this.config = { - treeDataSource: instantiationService.createInstance(NotebookOutlinePaneProvider, () => (this.entries ?? [])), - quickPickDataSource: instantiationService.createInstance(NotebookQuickPickProvider, () => (this.entries ?? [])), - breadcrumbsDataSource: instantiationService.createInstance(NotebookBreadcrumbsProvider, () => (this.activeElement)), + treeDataSource: this._treeDataSource, + quickPickDataSource: this._quickPickDataSource, + breadcrumbsDataSource: this._breadcrumbsDataSource, delegate, renderers, comparator, @@ -477,25 +591,150 @@ export class NotebookCellOutline implements IOutline { }; } - async setFullSymbols(cancelToken: CancellationToken) { - await this._outlineProviderReference?.object?.setFullSymbols(cancelToken); + private initializeOutline() { + // initial setup + this.setDataSources(); + this.setModelListeners(); + + // reset the data sources + model listeners when we get a new notebook model + this._disposables.add(this._editor.onDidChangeModel(() => { + this.setDataSources(); + this.setModelListeners(); + this.computeSymbols(); + })); + + // recompute symbols as document symbol providers are updated in the language features registry + this._disposables.add(this._languageFeaturesService.documentSymbolProvider.onDidChange(() => { + this.delayedComputeSymbols(); + })); + + // recompute active when the selection changes + this._disposables.add(this._editor.onDidChangeSelection(() => { + this.delayedRecomputeActive(); + })); + + // recompute state when filter config changes + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(NotebookSetting.outlineShowMarkdownHeadersOnly) || + e.affectsConfiguration(NotebookSetting.outlineShowCodeCells) || + e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols) || + e.affectsConfiguration(NotebookSetting.breadcrumbsShowCodeCells) + ) { + this.delayedRecomputeState(); + } + })); + + // recompute state when execution states change + this._disposables.add(this._notebookExecutionStateService.onDidChangeExecution(e => { + if (e.type === NotebookExecutionType.cell && !!this._editor.textModel && e.affectsNotebook(this._editor.textModel?.uri)) { + this.delayedRecomputeState(); + } + })); + + // recompute symbols when the configuration changes (recompute state - and therefore recompute active - is also called within compute symbols) + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(NotebookSetting.gotoSymbolsAllSymbols) || e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols)) { + this.gotoShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); + this.outlineShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); + this.computeSymbols(); + } + })); + + // fire a change event when the theme changes + this._disposables.add(this._themeService.onDidFileIconThemeChange(() => { + this._onDidChange.fire({}); + })); + + // finish with a recompute state + this.recomputeState(); } - get uri(): URI | undefined { - return this._outlineProviderReference?.object?.uri; + /** + * set up the primary data source + three viewing sources for the various outline views + */ + private setDataSources(): void { + const notebookEditor = this._editor.getControl(); + this._outlineDataSourceReference?.dispose(); + this._dataSourceDisposables.clear(); + + if (!notebookEditor?.hasModel()) { + this._outlineDataSourceReference = undefined; + } else { + this._outlineDataSourceReference = this._dataSourceDisposables.add(this._instantiationService.invokeFunction((accessor) => accessor.get(INotebookCellOutlineDataSourceFactory).getOrCreate(notebookEditor))); + // escalate outline data source change events + this._dataSourceDisposables.add(this._outlineDataSourceReference.object.onDidChange(() => { + this._onDidChange.fire({}); + })); + } + + // these fields can be passed undefined outlineDataSources. View Providers all handle it accordingly + this._treeDataSource = this._dataSourceDisposables.add(this._instantiationService.createInstance(NotebookOutlinePaneProvider, this._outlineDataSourceReference)); + this._quickPickDataSource = this._dataSourceDisposables.add(this._instantiationService.createInstance(NotebookQuickPickProvider, this._outlineDataSourceReference)); + this._breadcrumbsDataSource = this._dataSourceDisposables.add(this._instantiationService.createInstance(NotebookBreadcrumbsProvider, this._outlineDataSourceReference)); } - get isEmpty(): boolean { - return this._outlineProviderReference?.object?.isEmpty ?? true; + + /** + * set up the listeners for the outline content, these respond to model changes in the notebook + */ + private setModelListeners(): void { + this._modelDisposables.clear(); + if (!this._editor.textModel) { + return; + } + + // Perhaps this is the first time we're building the outline + if (!this.entries.length) { + this.computeSymbols(); + } + + // recompute state when there are notebook content changes + this._modelDisposables.add(this._editor.textModel.onDidChangeContent(contentChanges => { + if (contentChanges.rawEvents.some(c => + c.kind === NotebookCellsChangeType.ChangeCellContent || + c.kind === NotebookCellsChangeType.ChangeCellInternalMetadata || + c.kind === NotebookCellsChangeType.Move || + c.kind === NotebookCellsChangeType.ModelChange)) { + this.delayedRecomputeState(); + } + })); + } + + private async computeSymbols(cancelToken: CancellationToken = CancellationToken.None) { + if (this._target === OutlineTarget.QuickPick && this.gotoShowCodeCellSymbols) { + await this._outlineDataSourceReference?.object?.computeFullSymbols(cancelToken); + } else if (this._target === OutlineTarget.OutlinePane && this.outlineShowCodeCellSymbols) { + // No need to wait for this, we want the outline to show up quickly. + void this._outlineDataSourceReference?.object?.computeFullSymbols(cancelToken); + } + } + private async delayedComputeSymbols() { + this.delayerRecomputeState.cancel(); + this.delayerRecomputeActive.cancel(); + this.delayerRecomputeSymbols.trigger(() => { this.computeSymbols(); }); + } + + private recomputeState() { this._outlineDataSourceReference?.object?.recomputeState(); } + private delayedRecomputeState() { + this.delayerRecomputeActive.cancel(); // Active is always recomputed after a recomputing the State. + this.delayerRecomputeState.trigger(() => { this.recomputeState(); }); + } + + private recomputeActive() { this._outlineDataSourceReference?.object?.recomputeActive(); } + private delayedRecomputeActive() { + this.delayerRecomputeActive.trigger(() => { this.recomputeActive(); }); } + async reveal(entry: OutlineEntry, options: IEditorOptions, sideBySide: boolean): Promise { + const notebookEditorOptions: INotebookEditorOptions = { + ...options, + override: this._editor.input?.editorId, + cellRevealType: CellRevealType.NearTopIfOutsideViewport, + selection: entry.position, + viewState: undefined, + }; await this._editorService.openEditor({ resource: entry.cell.uri, - options: { - ...options, - override: this._editor.input?.editorId, - cellRevealType: CellRevealType.NearTopIfOutsideViewport, - selection: entry.position - } as INotebookEditorOptions, + options: notebookEditorOptions, }, sideBySide ? SIDE_GROUP : undefined); } @@ -562,10 +801,10 @@ export class NotebookCellOutline implements IOutline { dispose(): void { this._onDidChange.dispose(); - this._dispoables.dispose(); - this._entriesDisposables.dispose(); - this._outlineProviderReference?.dispose(); - this._localDisposables.dispose(); + this._disposables.dispose(); + this._modelDisposables.dispose(); + this._dataSourceDisposables.dispose(); + this._outlineDataSourceReference?.dispose(); } } @@ -576,7 +815,6 @@ export class NotebookOutlineCreator implements IOutlineCreator reg.dispose(); @@ -587,18 +825,7 @@ export class NotebookOutlineCreator implements IOutlineCreator | undefined> { - const outline = this._instantiationService.createInstance(NotebookCellOutline, editor, target); - - const showAllGotoSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); - const showAllOutlineSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); - if (target === OutlineTarget.QuickPick && showAllGotoSymbols) { - await outline.setFullSymbols(cancelToken); - } else if (target === OutlineTarget.OutlinePane && showAllOutlineSymbols) { - // No need to wait for this, we want the outline to show up quickly. - void outline.setFullSymbols(cancelToken); - } - - return outline; + return this._instantiationService.createInstance(NotebookCellOutline, editor, target); } } @@ -677,7 +904,6 @@ registerAction2(class ToggleShowMarkdownHeadersOnly extends Action2 { } }); - registerAction2(class ToggleCodeCellEntries extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts index eb2d57e62fe..d5899e31986 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts @@ -499,7 +499,7 @@ export class CodeActionParticipantUtils { }; for (const codeActionKind of codeActionsOnSave) { - const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, languageFeaturesService, getActionProgress, token); + const actionsToRun = await CodeActionParticipantUtils.getActionsToRun(model, codeActionKind, excludes, languageFeaturesService, getActionProgress, token); if (token.isCancellationRequested) { actionsToRun.dispose(); return; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts index 690a05c19fb..22b0a5e3fe3 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts @@ -98,11 +98,11 @@ export class TroubleshootController extends Disposable implements INotebookEdito items.push({ handle: i, items: [ - { + { text: `index: ${i}`, alignment: CellStatusbarAlignment.Left, priority: Number.MAX_SAFE_INTEGER - } + } satisfies INotebookCellStatusBarItem ] }); } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts index 1d5d4405f06..e69da8b479d 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts @@ -64,7 +64,7 @@ CommandsRegistry.registerCommand('_resolveNotebookKernels', async (accessor, arg }[]> => { const notebookKernelService = accessor.get(INotebookKernelService); const uri = URI.revive(args.uri as UriComponents); - const kernels = notebookKernelService.getMatchingKernel({ uri, viewType: args.viewType }); + const kernels = notebookKernelService.getMatchingKernel({ uri, notebookType: args.viewType }); return kernels.all.map(provider => ({ id: provider.id, diff --git a/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts index 0db8e6dd2b4..1350fefa69d 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts @@ -7,6 +7,7 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; import { INotebookOutputActionContext, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { NOTEBOOK_CELL_HAS_OUTPUTS } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; @@ -14,7 +15,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { copyCellOutput } from 'vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ICellOutputViewModel, ICellViewModel, INotebookEditor, getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; export const COPY_OUTPUT_COMMAND_ID = 'notebook.cellOutput.copy'; @@ -95,7 +96,7 @@ function getOutputViewModelFromId(outputId: string, notebookEditor: INotebookEdi if (notebookViewModel) { const codeCells = notebookViewModel.viewCells.filter(cell => cell.cellKind === CellKind.Code) as CodeCellViewModel[]; for (const cell of codeCells) { - const output = cell.outputsViewModels.find(output => output.model.outputId === outputId); + const output = cell.outputsViewModels.find(output => output.model.outputId === outputId || output.model.alternativeOutputId === outputId); if (output) { return output; } @@ -104,3 +105,45 @@ function getOutputViewModelFromId(outputId: string, notebookEditor: INotebookEdi return undefined; } + +export const OPEN_OUTPUT_COMMAND_ID = 'notebook.cellOutput.openInTextEditor'; + +registerAction2(class OpenCellOutputInEditorAction extends Action2 { + constructor() { + super({ + id: OPEN_OUTPUT_COMMAND_ID, + title: localize('notebookActions.openOutputInEditor', "Open Cell Output in Text Editor"), + f1: false, + category: NOTEBOOK_ACTIONS_CATEGORY, + icon: icons.copyIcon, + }); + } + + private getNoteboookEditor(editorService: IEditorService, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): INotebookEditor | undefined { + if (outputContext && 'notebookEditor' in outputContext) { + return outputContext.notebookEditor; + } + return getNotebookEditorFromEditorPane(editorService.activeEditorPane); + } + + async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise { + const notebookEditor = this.getNoteboookEditor(accessor.get(IEditorService), outputContext); + + if (!notebookEditor) { + return; + } + + let outputViewModel: ICellOutputViewModel | undefined; + if (outputContext && 'outputId' in outputContext && typeof outputContext.outputId === 'string') { + outputViewModel = getOutputViewModelFromId(outputContext.outputId, notebookEditor); + } else if (outputContext && 'outputViewModel' in outputContext) { + outputViewModel = outputContext.outputViewModel; + } + + const openerService = accessor.get(IOpenerService); + + if (outputViewModel?.model.outputId && notebookEditor.textModel?.uri) { + openerService.open(CellUri.generateCellOutputUri(notebookEditor.textModel.uri, outputViewModel.model.outputId)); + } + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts index 7ed5b3c5683..f474b4dfc6b 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts @@ -15,8 +15,8 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_HAS_AGENT, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; import { NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController'; import { CELL_TITLE_CELL_GROUP_ID, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, getEditorFromArgsOrActivePane } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { insertNewCell } from 'vs/workbench/contrib/notebook/browser/controller/insertCellActions'; @@ -24,7 +24,6 @@ import { CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBro import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_GENERATED_BY_CHAT, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; - registerAction2(class extends NotebookAction { constructor() { super( @@ -259,9 +258,9 @@ registerAction2(class extends NotebookAction { menu: [ { id: MENU_CELL_CHAT_WIDGET_STATUS, - group: 'inline', + group: '0_main', order: 0, - when: CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.OnlyMessages), + when: CTX_INLINE_CHAT_RESPONSE_TYPE.notEqualsTo(InlineChatResponseType.Messages), } ], f1: false @@ -287,7 +286,7 @@ registerAction2(class extends NotebookAction { }, menu: { id: MENU_CELL_CHAT_WIDGET_STATUS, - group: 'main', + group: '0_main', order: 1 }, f1: false @@ -367,7 +366,7 @@ registerAction2(class extends NotebookAction { NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), ContextKeyExpr.not(InputFocusedContextKey), - CTX_INLINE_CHAT_HAS_AGENT, + CTX_NOTEBOOK_CHAT_HAS_AGENT, ContextKeyExpr.or( ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true), ContextKeyExpr.equals(`config.${NotebookSetting.cellGenerate}`, true) @@ -384,7 +383,7 @@ registerAction2(class extends NotebookAction { order: -1, when: ContextKeyExpr.and( NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), - CTX_INLINE_CHAT_HAS_AGENT, + CTX_NOTEBOOK_CHAT_HAS_AGENT, ContextKeyExpr.or( ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true), ContextKeyExpr.equals(`config.${NotebookSetting.cellGenerate}`, true) @@ -459,7 +458,7 @@ registerAction2(class extends NotebookAction { order: -1, when: ContextKeyExpr.and( NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), - CTX_INLINE_CHAT_HAS_AGENT, + CTX_NOTEBOOK_CHAT_HAS_AGENT, ContextKeyExpr.or( ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true), ContextKeyExpr.equals(`config.${NotebookSetting.cellGenerate}`, true) @@ -488,7 +487,7 @@ MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), ContextKeyExpr.notEquals('config.notebook.insertToolbarLocation', 'betweenCells'), ContextKeyExpr.notEquals('config.notebook.insertToolbarLocation', 'hidden'), - CTX_INLINE_CHAT_HAS_AGENT, + CTX_NOTEBOOK_CHAT_HAS_AGENT, ContextKeyExpr.or( ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true), ContextKeyExpr.equals(`config.${NotebookSetting.cellGenerate}`, true) @@ -633,7 +632,7 @@ registerAction2(class extends NotebookCellAction { order: 0, when: ContextKeyExpr.and( NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), - CTX_INLINE_CHAT_HAS_AGENT, + CTX_NOTEBOOK_CHAT_HAS_AGENT, NOTEBOOK_CELL_GENERATED_BY_CHAT, ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true) ) diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts index f35264aa8cc..0460f9e57da 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts @@ -4,19 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import 'vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions'; +import { CTX_NOTEBOOK_CHAT_HAS_AGENT } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; import { NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; -class NotebookChatVariables extends Disposable implements IWorkbenchContribution { +class NotebookChatContribution extends Disposable implements IWorkbenchContribution { - static readonly ID = 'workbench.contrib.notebookChatVariables'; + static readonly ID = 'workbench.contrib.notebookChatContribution'; + + private readonly _ctxHasProvider: IContextKey; constructor( @IChatVariablesService private readonly _chatVariableService: IChatVariablesService, - @INotebookEditorService private readonly _notebookEditorService: INotebookEditorService + @INotebookEditorService private readonly _notebookEditorService: INotebookEditorService, + @IContextKeyService contextKeyService: IContextKeyService, + @IChatAgentService chatAgentService: IChatAgentService ) { super(); @@ -34,7 +41,17 @@ class NotebookChatVariables extends Disposable implements IWorkbenchContribution return undefined; } )); + + this._ctxHasProvider = CTX_NOTEBOOK_CHAT_HAS_AGENT.bindTo(contextKeyService); + + const updateNotebookAgentStatus = () => { + const hasNotebookAgent = Boolean(chatAgentService.getDefaultAgent(ChatAgentLocation.Notebook)); + this._ctxHasProvider.set(hasNotebookAgent); + }; + + updateNotebookAgentStatus(); + this._register(chatAgentService.onDidChangeAgents(updateNotebookAgentStatus)); } } -registerWorkbenchContribution2(NotebookChatVariables.ID, NotebookChatVariables, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(NotebookChatContribution.ID, NotebookChatContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext.ts index 4c8a6aa024d..259b8e8303e 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext.ts @@ -17,3 +17,5 @@ export const MENU_CELL_CHAT_WIDGET = MenuId.for('cellChatWidget'); export const MENU_CELL_CHAT_WIDGET_STATUS = MenuId.for('cellChatWidget.status'); export const MENU_CELL_CHAT_WIDGET_FEEDBACK = MenuId.for('cellChatWidget.feedback'); export const MENU_CELL_CHAT_WIDGET_TOOLBAR = MenuId.for('cellChatWidget.toolbar'); + +export const CTX_NOTEBOOK_CHAT_HAS_AGENT = new RawContextKey('notebookChatAgentRegistered', false, localize('notebookChatAgentRegistered', "Whether a chat agent for notebook is registered")); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index 4662ab11501..fc164d21321 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -26,7 +26,6 @@ import { ICursorStateComputer, ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IModelService } from 'vs/editor/common/services/model'; import { localize } from 'vs/nls'; -import { MenuId } from 'vs/platform/actions/common/actions'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -37,7 +36,6 @@ import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/utils'; -import { MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { insertCell, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; import { ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookViewZone } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -415,14 +413,16 @@ export class NotebookChatController extends Disposable implements INotebookEdito InlineChatWidget, ChatAgentLocation.Notebook, { - telemetrySource: 'notebook-generate-cell', - inputMenuId: MenuId.ChatExecute, - widgetMenuId: MENU_INLINE_CHAT_WIDGET, statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, - rendererOptions: { - renderTextEditsAsSummary: (uri) => { - return isEqual(uri, this._widget?.parentEditor.getModel()?.uri) - || isEqual(uri, this._notebookEditor.textModel?.uri); + chatWidgetViewOptions: { + rendererOptions: { + renderTextEditsAsSummary: (uri) => { + return isEqual(uri, this._widget?.parentEditor.getModel()?.uri) + || isEqual(uri, this._notebookEditor.textModel?.uri); + } + }, + menus: { + telemetrySource: 'notebook-generate-cell' } } } @@ -469,6 +469,10 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._sessionCtor = createCancelablePromise(async token => { await this._startSession(token); + assertType(this._model.value); + const model = this._model.value; + this._widget?.inlineChatWidget.setChatModel(model); + if (fakeParentEditor.hasModel()) { if (this._widget) { @@ -546,9 +550,6 @@ export class NotebookChatController extends Disposable implements INotebookEdito assertType(this._model.value); assertType(this._strategy); - const model = this._model.value; - this._widget.inlineChatWidget.setChatModel(model); - const lastInput = this._widget.inlineChatWidget.value; this._historyUpdate(lastInput); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts index 00c89e900fa..ea94a04d2ad 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts @@ -181,7 +181,7 @@ export class OutputElement extends Disposable { this.resizeListener.clear(); const element = this.domNode; if (element) { - element.parentElement?.removeChild(element); + element.remove(); this._notebookEditor.removeInset( this._diffElementViewModel, this._nestedCell, @@ -259,7 +259,7 @@ export class OutputContainer extends Disposable { // already removed removedKeys.push(key); // remove element from DOM - this._outputContainer.removeChild(value.domNode); + value.domNode.remove(); this._editor.removeInset(this._diffElementViewModel, this._nestedCellViewModel, key, this._diffSide); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index 29dc02777d3..dd98ad9d3a4 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -41,13 +41,11 @@ import { BackLayerWebView, INotebookDelegateForWebview } from 'vs/workbench/cont import { NotebookDiffEditorEventDispatcher, NotebookDiffLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; import { FontMeasurements } from 'vs/editor/browser/config/fontMeasurements'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; -import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { cellIndexesToRanges, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { NotebookDiffOverviewRuler } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffOverviewRuler'; import { registerZIndex, ZIndex } from 'vs/platform/layout/browser/zIndexRegistry'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; const $ = DOM.$; @@ -151,11 +149,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, - @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, - @ICodeEditorService codeEditorService: ICodeEditorService ) { super(NotebookTextDiffEditor.ID, group, telemetryService, themeService, storageService); - this._notebookOptions = new NotebookOptions(this.window, this.configurationService, notebookExecutionStateService, codeEditorService, false); + this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, false, undefined); this._register(this._notebookOptions); this._revealFirst = true; } diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css index 4577677c02a..68974e01dc1 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css @@ -188,14 +188,6 @@ user-select: text; } -.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .markdownMessage .message[state="cropped"] { - -webkit-line-clamp: var(--vscode-inline-chat-cropped, 3); -} - -.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .markdownMessage .message[state="expanded"] { - -webkit-line-clamp: var(--vscode-inline-chat-expanded, 10); -} - .monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .label A { color: var(--vscode-textLink-foreground); cursor: pointer; @@ -352,4 +344,3 @@ .monaco-workbench .notebookOverlay .cell-chat-part .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { opacity: 1; } - diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 9f45a4c5c28..7d1b5f751f9 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -57,7 +57,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/browser/services/notebookRendererMessagingServiceImpl'; import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; -import { INotebookCellOutlineProviderFactory, NotebookCellOutlineProviderFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory'; +import { INotebookCellOutlineDataSourceFactory, NotebookCellOutlineDataSourceFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory'; // Editor Controller import 'vs/workbench/contrib/notebook/browser/controller/coreActions'; @@ -192,8 +192,8 @@ class NotebookDiffEditorSerializer implements IEditorSerializer { } type SerializedNotebookEditorData = { resource: URI; preferredResource: URI; viewType: string; options?: NotebookEditorInputOptions }; class NotebookEditorSerializer implements IEditorSerializer { - canSerialize(): boolean { - return true; + canSerialize(input: EditorInput): boolean { + return input.typeId === NotebookEditorInput.ID; } serialize(input: EditorInput): string { assertType(input instanceof NotebookEditorInput); @@ -665,7 +665,7 @@ class SimpleNotebookWorkingCopyEditorHandler extends Disposable implements IWork private handlesSync(workingCopy: IWorkingCopyIdentifier): string /* viewType */ | undefined { const viewType = this._getViewType(workingCopy); - if (!viewType || viewType === 'interactive') { + if (!viewType || viewType === 'interactive' || extname(workingCopy.resource) === '.replNotebook') { return undefined; } @@ -757,7 +757,7 @@ registerSingleton(INotebookExecutionStateService, NotebookExecutionStateService, registerSingleton(INotebookRendererMessagingService, NotebookRendererMessagingService, InstantiationType.Delayed); registerSingleton(INotebookKeymapService, NotebookKeymapService, InstantiationType.Delayed); registerSingleton(INotebookLoggingService, NotebookLoggingService, InstantiationType.Delayed); -registerSingleton(INotebookCellOutlineProviderFactory, NotebookCellOutlineProviderFactory, InstantiationType.Delayed); +registerSingleton(INotebookCellOutlineDataSourceFactory, NotebookCellOutlineDataSourceFactory, InstantiationType.Delayed); const schemas: IJSONSchemaMap = {}; function isConfigurationPropertySchema(x: IConfigurationPropertySchema | { [path: string]: IConfigurationPropertySchema }): x is IConfigurationPropertySchema { @@ -1086,11 +1086,6 @@ configurationRegistry.registerConfiguration({ }, tags: ['notebookLayout'] }, - [NotebookSetting.findScope]: { - markdownDescription: nls.localize('notebook.experimental.find.scope.enabled', "Enables the user to search within a selection of cells in the notebook. When enabled, the user will see a \"Find in Cell Selection\" icon in the notebook find widget."), - type: 'boolean', - default: false, - }, [NotebookSetting.remoteSaving]: { markdownDescription: nls.localize('notebook.remoteSaving', "Enables the incremental saving of notebooks between processes and across Remote connections. When enabled, only the changes to the notebook are sent to the extension host, improving performance for large notebooks and slow network connections."), type: 'boolean', diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider.ts index 948db4cd3a1..33a38169f39 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider.ts @@ -46,7 +46,7 @@ export class NotebookAccessibilityProvider extends Disposable implements IListAc getAriaLabel(element: CellViewModel) { const event = Event.filter(this.onDidAriaLabelChange, e => e === element); - return observableFromEvent(event, () => { + return observableFromEvent(this, event, () => { const viewModel = this.viewModel(); if (!viewModel) { return ''; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index f474d202cb1..18e44f2a663 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -22,7 +22,7 @@ import { IEditorPane, IEditorPaneWithSelection } from 'vs/workbench/common/edito import { CellViewModelStateChangeEvent, NotebookCellStateChangedEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, ICellOutput, INotebookCellStatusBarItem, INotebookRendererInfo, INotebookSearchOptions, IOrderedMimeType, NotebookCellInternalMetadata, NotebookCellMetadata, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ICellOutput, INotebookCellStatusBarItem, INotebookRendererInfo, INotebookFindOptions, IOrderedMimeType, NotebookCellInternalMetadata, NotebookCellMetadata, NOTEBOOK_EDITOR_ID, REPL_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { isCompositeNotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; @@ -383,6 +383,7 @@ export interface INotebookEditorCreationOptions { }; readonly options?: NotebookOptions; readonly codeWindow?: CodeWindow; + readonly forRepl?: boolean; } export interface INotebookWebviewMessage { @@ -448,7 +449,7 @@ export interface INotebookViewCellsUpdateEvent { export interface INotebookViewModel { notebookDocument: NotebookTextModel; - viewCells: ICellViewModel[]; + readonly viewCells: ICellViewModel[]; layoutInfo: NotebookLayoutInfo | null; onDidChangeViewCells: Event; onDidChangeSelection: Event; @@ -737,7 +738,7 @@ export interface INotebookEditor { getCellIndex(cell: ICellViewModel): number | undefined; getNextVisibleCellIndex(index: number): number | undefined; getPreviousVisibleCellIndex(index: number): number | undefined; - find(query: string, options: INotebookSearchOptions, token: CancellationToken, skipWarmup?: boolean, shouldGetSearchPreviewInfo?: boolean, ownerID?: string): Promise; + find(query: string, options: INotebookFindOptions, token: CancellationToken, skipWarmup?: boolean, shouldGetSearchPreviewInfo?: boolean, ownerID?: string): Promise; findHighlightCurrent(matchIndex: number, ownerID?: string): Promise; findUnHighlightCurrent(matchIndex: number, ownerID?: string): Promise; findStop(ownerID?: string): void; @@ -878,7 +879,9 @@ export function getNotebookEditorFromEditorPane(editorPane?: IEditorPane): INote const input = editorPane.input; - if (input && isCompositeNotebookEditorInput(input)) { + const isInteractiveEditor = input && isCompositeNotebookEditorInput(input); + + if (isInteractiveEditor || editorPane.getId() === REPL_EDITOR_ID) { return (editorPane.getControl() as { notebookEditor: INotebookEditor | undefined } | undefined)?.notebookEditor; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 1727d1fb60c..b2df5bfdf41 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -74,7 +74,7 @@ import { NotebookEditorContextKeys } from 'vs/workbench/contrib/notebook/browser import { NotebookOverviewRuler } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler'; import { ListTopCellToolbar } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellEditType, CellKind, INotebookSearchOptions, RENDERER_NOT_AVAILABLE, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, INotebookFindOptions, NotebookFindScopeType, RENDERER_NOT_AVAILABLE, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NOTEBOOK_CURSOR_NAVIGATION_MODE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_OUTPUT_INPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; @@ -99,7 +99,6 @@ import { NotebookStickyScroll } from 'vs/workbench/contrib/notebook/browser/view import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { PixelRatio } from 'vs/base/browser/pixelRatio'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { PreventDefaultContextMenuItemsContextKeyName } from 'vs/workbench/contrib/webview/browser/webview.contribution'; import { NotebookAccessibilityProvider } from 'vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider'; @@ -272,6 +271,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD readonly isEmbedded: boolean; private _readOnly: boolean; + private readonly _inRepl: boolean; public readonly scopedContextKeyService: IContextKeyService; private readonly instantiationService: IInstantiationService; @@ -302,7 +302,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD @IEditorProgressService private editorProgressService: IEditorProgressService, @INotebookLoggingService private readonly logService: INotebookLoggingService, @IKeybindingService private readonly keybindingService: IKeybindingService, - @ICodeEditorService codeEditorService: ICodeEditorService ) { super(); @@ -310,8 +309,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this.isEmbedded = creationOptions.isEmbedded ?? false; this._readOnly = creationOptions.isReadOnly ?? false; + this._inRepl = creationOptions.forRepl ?? false; - this._notebookOptions = creationOptions.options ?? new NotebookOptions(this.creationOptions?.codeWindow ?? mainWindow, this.configurationService, notebookExecutionStateService, codeEditorService, this._readOnly); + this._overlayContainer = document.createElement('div'); + this.scopedContextKeyService = this._register(contextKeyService.createScoped(this._overlayContainer)); + this.instantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); + + this._notebookOptions = creationOptions.options ?? + this.instantiationService.createInstance(NotebookOptions, this.creationOptions?.codeWindow ?? mainWindow, this._readOnly, undefined); this._register(this._notebookOptions); const eventDispatcher = this._register(new NotebookEventDispatcher()); this._viewContext = new ViewContext( @@ -322,9 +327,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._onDidChangeCellState.fire(e); })); - this._overlayContainer = document.createElement('div'); - this.scopedContextKeyService = this._register(contextKeyService.createScoped(this._overlayContainer)); - this.instantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); this._register(_notebookService.onDidChangeOutputRenderers(() => { this._updateOutputRenderers(); @@ -1435,7 +1437,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD private async _attachModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined, perf?: NotebookPerfMarks) { this._ensureWebview(this.getId(), textModel.viewType, textModel.uri); - this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this._viewContext, this.getLayoutInfo(), { isReadOnly: this._readOnly }); + this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this._viewContext, this.getLayoutInfo(), { isReadOnly: this._readOnly, inRepl: this._inRepl }); this._viewContext.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); this.notebookOptions.updateOptions(this._readOnly); @@ -1851,6 +1853,19 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return; } + const whenContainerStylesLoaded = this.layoutService.whenContainerStylesLoaded(DOM.getWindow(this.getDomNode())); + if (whenContainerStylesLoaded) { + // In floating windows, we need to ensure that the + // container is ready for us to compute certain + // layout related properties. + whenContainerStylesLoaded.then(() => this.layoutNotebook(dimension, shadowElement, position)); + } else { + this.layoutNotebook(dimension, shadowElement, position); + } + + } + + private layoutNotebook(dimension: DOM.Dimension, shadowElement?: HTMLElement, position?: DOM.IDomPosition) { if (shadowElement) { this.updateShadowElement(shadowElement, dimension, position); } @@ -2577,7 +2592,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return Promise.all(requests); } - async find(query: string, options: INotebookSearchOptions, token: CancellationToken, skipWarmup: boolean = false, shouldGetSearchPreviewInfo = false, ownerID?: string): Promise { + async find(query: string, options: INotebookFindOptions, token: CancellationToken, skipWarmup: boolean = false, shouldGetSearchPreviewInfo = false, ownerID?: string): Promise { if (!this._notebookViewModel) { return []; } @@ -2588,7 +2603,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD const findMatches = this._notebookViewModel.find(query, options).filter(match => match.length > 0); - if (!options.includeMarkupPreview && !options.includeOutput) { + if ((!options.includeMarkupPreview && !options.includeOutput) || options.findScope?.findScopeType === NotebookFindScopeType.Text) { this._webview?.findStop(ownerID); return findMatches; } @@ -2612,11 +2627,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return []; } - const selectedRanges = options.selectedRanges?.map(range => this._notebookViewModel?.validateRange(range)).filter(range => !!range); - const selectedIndexes = cellRangesToIndexes(selectedRanges ?? []); - const findIds: string[] = selectedIndexes.map(index => this._notebookViewModel?.viewCells[index].id ?? ''); + let findIds: string[] = []; + if (options.findScope && options.findScope.findScopeType === NotebookFindScopeType.Cells && options.findScope.selectedCellRanges) { + const selectedIndexes = cellRangesToIndexes(options.findScope.selectedCellRanges); + findIds = selectedIndexes.map(index => this._notebookViewModel?.viewCells[index].id ?? ''); + } - const webviewMatches = await this._webview.find(query, { caseSensitive: options.caseSensitive, wholeWord: options.wholeWord, includeMarkup: !!options.includeMarkupPreview, includeOutput: !!options.includeOutput, shouldGetSearchPreviewInfo, ownerID, findIds: options.searchInRanges ? findIds : [] }); + const webviewMatches = await this._webview.find(query, { caseSensitive: options.caseSensitive, wholeWord: options.wholeWord, includeMarkup: !!options.includeMarkupPreview, includeOutput: !!options.includeOutput, shouldGetSearchPreviewInfo, ownerID, findIds: findIds }); if (token.isCancellationRequested) { return []; @@ -3235,54 +3252,19 @@ export const notebookCellBorder = registerColor('notebook.cellBorderColor', { hcLight: PANEL_BORDER }, nls.localize('notebook.cellBorderColor', "The border color for notebook cells.")); -export const focusedEditorBorderColor = registerColor('notebook.focusedEditorBorder', { - light: focusBorder, - dark: focusBorder, - hcDark: focusBorder, - hcLight: focusBorder -}, nls.localize('notebook.focusedEditorBorder', "The color of the notebook cell editor border.")); - -export const cellStatusIconSuccess = registerColor('notebookStatusSuccessIcon.foreground', { - light: debugIconStartForeground, - dark: debugIconStartForeground, - hcDark: debugIconStartForeground, - hcLight: debugIconStartForeground -}, nls.localize('notebookStatusSuccessIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); - -export const runningCellRulerDecorationColor = registerColor('notebookEditorOverviewRuler.runningCellForeground', { - light: debugIconStartForeground, - dark: debugIconStartForeground, - hcDark: debugIconStartForeground, - hcLight: debugIconStartForeground -}, nls.localize('notebookEditorOverviewRuler.runningCellForeground', "The color of the running cell decoration in the notebook editor overview ruler.")); - -export const cellStatusIconError = registerColor('notebookStatusErrorIcon.foreground', { - light: errorForeground, - dark: errorForeground, - hcDark: errorForeground, - hcLight: errorForeground -}, nls.localize('notebookStatusErrorIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); - -export const cellStatusIconRunning = registerColor('notebookStatusRunningIcon.foreground', { - light: foreground, - dark: foreground, - hcDark: foreground, - hcLight: foreground -}, nls.localize('notebookStatusRunningIcon.foreground', "The running icon color of notebook cells in the cell status bar.")); - -export const notebookOutputContainerBorderColor = registerColor('notebook.outputContainerBorderColor', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, nls.localize('notebook.outputContainerBorderColor', "The border color of the notebook output container.")); +export const focusedEditorBorderColor = registerColor('notebook.focusedEditorBorder', focusBorder, nls.localize('notebook.focusedEditorBorder', "The color of the notebook cell editor border.")); -export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, nls.localize('notebook.outputContainerBackgroundColor', "The color of the notebook output container background.")); +export const cellStatusIconSuccess = registerColor('notebookStatusSuccessIcon.foreground', debugIconStartForeground, nls.localize('notebookStatusSuccessIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); + +export const runningCellRulerDecorationColor = registerColor('notebookEditorOverviewRuler.runningCellForeground', debugIconStartForeground, nls.localize('notebookEditorOverviewRuler.runningCellForeground', "The color of the running cell decoration in the notebook editor overview ruler.")); + +export const cellStatusIconError = registerColor('notebookStatusErrorIcon.foreground', errorForeground, nls.localize('notebookStatusErrorIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); + +export const cellStatusIconRunning = registerColor('notebookStatusRunningIcon.foreground', foreground, nls.localize('notebookStatusRunningIcon.foreground', "The running icon color of notebook cells in the cell status bar.")); + +export const notebookOutputContainerBorderColor = registerColor('notebook.outputContainerBorderColor', null, nls.localize('notebook.outputContainerBorderColor', "The border color of the notebook output container.")); + +export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', null, nls.localize('notebook.outputContainerBackgroundColor', "The color of the notebook output container background.")); // TODO@rebornix currently also used for toolbar border, if we keep all of this, pick a generic name export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeparator', { @@ -3292,12 +3274,7 @@ export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeparat hcLight: contrastBorder }, nls.localize('notebook.cellToolbarSeparator', "The color of the separator in the cell bottom toolbar")); -export const focusedCellBackground = registerColor('notebook.focusedCellBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, nls.localize('focusedCellBackground', "The background color of a cell when the cell is focused.")); +export const focusedCellBackground = registerColor('notebook.focusedCellBackground', null, nls.localize('focusedCellBackground', "The background color of a cell when the cell is focused.")); export const selectedCellBackground = registerColor('notebook.selectedCellBackground', { dark: listInactiveSelectionBackground, @@ -3328,19 +3305,9 @@ export const inactiveSelectedCellBorder = registerColor('notebook.inactiveSelect hcLight: focusBorder }, nls.localize('notebook.inactiveSelectedCellBorder', "The color of the cell's borders when multiple cells are selected.")); -export const focusedCellBorder = registerColor('notebook.focusedCellBorder', { - dark: focusBorder, - light: focusBorder, - hcDark: focusBorder, - hcLight: focusBorder -}, nls.localize('notebook.focusedCellBorder', "The color of the cell's focus indicator borders when the cell is focused.")); +export const focusedCellBorder = registerColor('notebook.focusedCellBorder', focusBorder, nls.localize('notebook.focusedCellBorder', "The color of the cell's focus indicator borders when the cell is focused.")); -export const inactiveFocusedCellBorder = registerColor('notebook.inactiveFocusedCellBorder', { - dark: notebookCellBorder, - light: notebookCellBorder, - hcDark: notebookCellBorder, - hcLight: notebookCellBorder -}, nls.localize('notebook.inactiveFocusedCellBorder', "The color of the cell's top and bottom border when a cell is focused while the primary focus is outside of the editor.")); +export const inactiveFocusedCellBorder = registerColor('notebook.inactiveFocusedCellBorder', notebookCellBorder, nls.localize('notebook.inactiveFocusedCellBorder', "The color of the cell's top and bottom border when a cell is focused while the primary focus is outside of the editor.")); export const cellStatusBarItemHover = registerColor('notebook.cellStatusBarItemHoverBackground', { light: new Color(new RGBA(0, 0, 0, 0.08)), @@ -3349,33 +3316,13 @@ export const cellStatusBarItemHover = registerColor('notebook.cellStatusBarItemH hcLight: new Color(new RGBA(0, 0, 0, 0.08)), }, nls.localize('notebook.cellStatusBarItemHoverBackground', "The background color of notebook cell status bar items.")); -export const cellInsertionIndicator = registerColor('notebook.cellInsertionIndicator', { - light: focusBorder, - dark: focusBorder, - hcDark: focusBorder, - hcLight: focusBorder -}, nls.localize('notebook.cellInsertionIndicator', "The color of the notebook cell insertion indicator.")); - -export const listScrollbarSliderBackground = registerColor('notebookScrollbarSlider.background', { - dark: scrollbarSliderBackground, - light: scrollbarSliderBackground, - hcDark: scrollbarSliderBackground, - hcLight: scrollbarSliderBackground -}, nls.localize('notebookScrollbarSliderBackground', "Notebook scrollbar slider background color.")); - -export const listScrollbarSliderHoverBackground = registerColor('notebookScrollbarSlider.hoverBackground', { - dark: scrollbarSliderHoverBackground, - light: scrollbarSliderHoverBackground, - hcDark: scrollbarSliderHoverBackground, - hcLight: scrollbarSliderHoverBackground -}, nls.localize('notebookScrollbarSliderHoverBackground', "Notebook scrollbar slider background color when hovering.")); - -export const listScrollbarSliderActiveBackground = registerColor('notebookScrollbarSlider.activeBackground', { - dark: scrollbarSliderActiveBackground, - light: scrollbarSliderActiveBackground, - hcDark: scrollbarSliderActiveBackground, - hcLight: scrollbarSliderActiveBackground -}, nls.localize('notebookScrollbarSliderActiveBackground', "Notebook scrollbar slider background color when clicked on.")); +export const cellInsertionIndicator = registerColor('notebook.cellInsertionIndicator', focusBorder, nls.localize('notebook.cellInsertionIndicator', "The color of the notebook cell insertion indicator.")); + +export const listScrollbarSliderBackground = registerColor('notebookScrollbarSlider.background', scrollbarSliderBackground, nls.localize('notebookScrollbarSliderBackground', "Notebook scrollbar slider background color.")); + +export const listScrollbarSliderHoverBackground = registerColor('notebookScrollbarSlider.hoverBackground', scrollbarSliderHoverBackground, nls.localize('notebookScrollbarSliderHoverBackground', "Notebook scrollbar slider background color when hovering.")); + +export const listScrollbarSliderActiveBackground = registerColor('notebookScrollbarSlider.activeBackground', scrollbarSliderActiveBackground, nls.localize('notebookScrollbarSliderActiveBackground', "Notebook scrollbar slider background color when clicked on.")); export const cellSymbolHighlight = registerColor('notebook.symbolHighlightBackground', { dark: Color.fromHex('#ffffff0b'), diff --git a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts index 20ffe3867e8..63c68389080 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts @@ -9,6 +9,7 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; export const selectKernelIcon = registerIcon('notebook-kernel-select', Codicon.serverEnvironment, localize('selectKernelIcon', 'Configure icon to select a kernel in notebook editors.')); export const executeIcon = registerIcon('notebook-execute', Codicon.play, localize('executeIcon', 'Icon to execute in notebook editors.')); +export const configIcon = registerIcon('notebook-config', Codicon.gear, localize('configIcon', 'Icon to configure in notebook editors.')); export const executeAboveIcon = registerIcon('notebook-execute-above', Codicon.runAbove, localize('executeAboveIcon', 'Icon to execute above cells in notebook editors.')); export const executeBelowIcon = registerIcon('notebook-execute-below', Codicon.runBelow, localize('executeBelowIcon', 'Icon to execute below cells in notebook editors.')); export const stopIcon = registerIcon('notebook-stop', Codicon.primitiveSquare, localize('stopIcon', 'Icon to stop an execution in notebook editors.')); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts index 46ce122a004..c83be8c872f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts @@ -137,11 +137,11 @@ export class NotebookOptions extends Disposable { constructor( readonly targetWindow: CodeWindow, - private readonly configurationService: IConfigurationService, - private readonly notebookExecutionStateService: INotebookExecutionStateService, - private readonly codeEditorService: ICodeEditorService, private isReadonly: boolean, - private readonly overrides?: { cellToolbarInteraction: string; globalToolbar: boolean; stickyScrollEnabled: boolean; dragAndDropEnabled: boolean } + private readonly overrides: { cellToolbarInteraction: string; globalToolbar: boolean; stickyScrollEnabled: boolean; dragAndDropEnabled: boolean } | undefined, + @IConfigurationService private readonly configurationService: IConfigurationService, + @INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, ) { super(); const showCellStatusBar = this.configurationService.getValue(NotebookSetting.showCellStatusBar); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts index 4be07da63b7..5b1360f0ca8 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts @@ -19,6 +19,8 @@ import { URI } from 'vs/base/common/uri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { InteractiveWindowOpen } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IEditorProgressService } from 'vs/platform/progress/common/progress'; export class NotebookEditorWidgetService implements INotebookEditorService { @@ -39,7 +41,8 @@ export class NotebookEditorWidgetService implements INotebookEditorService { constructor( @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { const groupListener = new Map(); @@ -182,9 +185,13 @@ export class NotebookEditorWidgetService implements INotebookEditorService { if (!value) { // NEW widget - const instantiationService = accessor.get(IInstantiationService); + const editorGroupContextKeyService = accessor.get(IContextKeyService); + const editorGroupEditorProgressService = accessor.get(IEditorProgressService); + const notebookInstantiationService = this.instantiationService.createChild(new ServiceCollection( + [IContextKeyService, editorGroupContextKeyService], + [IEditorProgressService, editorGroupEditorProgressService])); const ctorOptions = creationOptions ?? getDefaultNotebookCreationOptions(); - const widget = instantiationService.createInstance(NotebookEditorWidget, { + const widget = notebookInstantiationService.createInstance(NotebookEditorWidget, { ...ctorOptions, codeWindow: codeWindow ?? ctorOptions.codeWindow, }, initialDimension); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts index 3e3446349b8..0c3852191da 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts @@ -48,7 +48,7 @@ export class NotebookKernelHistoryService extends Disposable implements INoteboo // We will suggest the only kernel const suggested = allAvailableKernels.all.length === 1 ? allAvailableKernels.all[0] : undefined; this._notebookLoggingService.debug('History', `getMatchingKernels: ${allAvailableKernels.all.length} kernels available for ${notebook.uri.path}. Selected: ${allAvailableKernels.selected?.label}. Suggested: ${suggested?.label}`); - const mostRecentKernelIds = this._mostRecentKernelsMap[notebook.viewType] ? [...this._mostRecentKernelsMap[notebook.viewType].values()] : []; + const mostRecentKernelIds = this._mostRecentKernelsMap[notebook.notebookType] ? [...this._mostRecentKernelsMap[notebook.notebookType].values()] : []; const all = mostRecentKernelIds.map(kernelId => allKernels.find(kernel => kernel.id === kernelId)).filter(kernel => !!kernel) as INotebookKernel[]; this._notebookLoggingService.debug('History', `mru: ${mostRecentKernelIds.length} kernels in history, ${all.length} registered already.`); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts index 572a833e173..05174803267 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts @@ -37,12 +37,12 @@ class KernelInfo { class NotebookTextModelLikeId { static str(k: INotebookTextModelLike): string { - return `${k.viewType}/${k.uri.toString()}`; + return `${k.notebookType}/${k.uri.toString()}`; } static obj(s: string): INotebookTextModelLike { const idx = s.indexOf('/'); return { - viewType: s.substring(0, idx), + notebookType: s.substring(0, idx), uri: URI.parse(s.substring(idx + 1)) }; } @@ -178,7 +178,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel private static _score(kernel: INotebookKernel, notebook: INotebookTextModelLike): number { if (kernel.viewType === '*') { return 5; - } else if (kernel.viewType === notebook.viewType) { + } else if (kernel.viewType === notebook.notebookType) { return 10; } else { return 0; @@ -343,7 +343,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel const stateChangeListener = sourceAction.onDidChangeState(() => { this._onDidChangeSourceActions.fire({ notebook: document.uri, - viewType: document.viewType, + viewType: document.notebookType, }); }); sourceActions.push([sourceAction, stateChangeListener]); @@ -351,7 +351,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel }); info.actions = sourceActions; this._kernelSources.set(id, info); - this._onDidChangeSourceActions.fire({ notebook: document.uri, viewType: document.viewType }); + this._onDidChangeSourceActions.fire({ notebook: document.uri, viewType: document.notebookType }); }; this._kernelSourceActionsUpdates.get(id)?.dispose(); @@ -382,7 +382,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel } getKernelDetectionTasks(notebook: INotebookTextModelLike): INotebookKernelDetectionTask[] { - return this._kernelDetectionTasks.get(notebook.viewType) ?? []; + return this._kernelDetectionTasks.get(notebook.notebookType) ?? []; } registerKernelSourceActionProvider(viewType: string, provider: IKernelSourceActionProvider): IDisposable { @@ -411,7 +411,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel * Get kernel source actions from providers */ getKernelSourceActions2(notebook: INotebookTextModelLike): Promise { - const viewType = notebook.viewType; + const viewType = notebook.notebookType; const providers = this._kernelSourceActionProviders.get(viewType) ?? []; const promises = providers.map(provider => provider.provideKernelSourceActions()); return Promise.all(promises).then(actions => { diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index 92cd870fc73..2b1dd162ad1 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -138,7 +138,6 @@ export class NotebookProviderInfoStore extends Disposable { selectors: notebookContribution.selector || [], priority: this._convertPriority(notebookContribution.priority), providerDisplayName: extension.description.displayName ?? extension.description.identifier.value, - exclusive: false })); } } @@ -177,7 +176,7 @@ export class NotebookProviderInfoStore extends Disposable { id: notebookProviderInfo.id, label: notebookProviderInfo.displayName, detail: notebookProviderInfo.providerDisplayName, - priority: notebookProviderInfo.exclusive ? RegisteredEditorPriority.exclusive : notebookProviderInfo.priority, + priority: notebookProviderInfo.priority, }; const notebookEditorOptions = { canHandleDiff: () => !!this._configurationService.getValue(NotebookSetting.textDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized(), @@ -203,7 +202,7 @@ export class NotebookProviderInfoStore extends Disposable { cellOptions = (options as INotebookEditorOptions | undefined)?.cellOptions; } - const notebookOptions = { ...options, cellOptions } as INotebookEditorOptions; + const notebookOptions: INotebookEditorOptions = { ...options, cellOptions, viewState: undefined }; // --- Start Positron --- if (getShouldUsePositronEditor(this._configurationService)) { // Use our editor instead of the built in one. @@ -681,8 +680,7 @@ export class NotebookService extends Disposable implements INotebookService { id: viewType, displayName: data.displayName, providerDisplayName: data.providerDisplayName, - exclusive: data.exclusive, - priority: RegisteredEditorPriority.default, + priority: data.priority || RegisteredEditorPriority.default, selectors: [] }); @@ -739,6 +737,14 @@ export class NotebookService extends Disposable implements INotebookService { return result; } + tryGetDataProviderSync(viewType: string): SimpleNotebookProviderInfo | undefined { + const selected = this.notebookProviderInfoStore.get(viewType); + if (!selected) { + return undefined; + } + return this._notebookProviders.get(selected.id); + } + private _persistMementos(): void { this._memento.saveMemento(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts index 854358af69d..bfb757be79c 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts @@ -17,7 +17,7 @@ import { MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/ac import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; export class CodiconActionViewItem extends MenuEntryActionViewItem { @@ -49,7 +49,7 @@ export class ActionViewWithLabel extends MenuEntryActionViewItem { } export class UnifiedSubmenuActionView extends SubmenuEntryActionViewItem { private _actionLabel?: HTMLAnchorElement; - private _hover?: IUpdatableHover; + private _hover?: IManagedHover; private _primaryAction: IAction | undefined; constructor( @@ -73,7 +73,7 @@ export class UnifiedSubmenuActionView extends SubmenuEntryActionViewItem { this._actionLabel = document.createElement('a'); container.appendChild(this._actionLabel); - this._hover = this._register(this._hoverService.setupUpdatableHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('element'), this._actionLabel, '')); + this._hover = this._register(this._hoverService.setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('element'), this._actionLabel, '')); this.updateLabel(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts index b8ccbf07abf..44e5866b04f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce } from 'vs/base/common/arrays'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import * as languages from 'vs/editor/common/languages'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -20,15 +20,13 @@ import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; export class CellComments extends CellContentPart { - private _initialized: boolean = false; - private _commentThreadWidget: CommentThreadWidget | null = null; + private readonly _commentThreadWidget: MutableDisposable>; private currentElement: CodeCellViewModel | undefined; - private readonly commentTheadDisposables = this._register(new DisposableStore()); + private readonly _commentThreadDisposables = this._register(new DisposableStore()); constructor( private readonly notebookEditor: INotebookEditorDelegate, private readonly container: HTMLElement, - @IContextKeyService private readonly contextKeyService: IContextKeyService, @IThemeService private readonly themeService: IThemeService, @ICommentService private readonly commentService: ICommentService, @@ -38,6 +36,8 @@ export class CellComments extends CellContentPart { super(); this.container.classList.add('review-widget'); + this._register(this._commentThreadWidget = new MutableDisposable>()); + this._register(this.themeService.onDidColorThemeChange(this._applyTheme, this)); // TODO @rebornix onDidChangeLayout (font change) // this._register(this.notebookEditor.onDidchangeLa) @@ -45,22 +45,17 @@ export class CellComments extends CellContentPart { } private async initialize(element: ICellViewModel) { - if (this._initialized) { + if (this.currentElement === element) { return; } - this._initialized = true; - const info = await this._getCommentThreadForCell(element); - - if (info) { - await this._createCommentTheadWidget(info.owner, info.thread); - } + this.currentElement = element as CodeCellViewModel; + await this._updateThread(); } private async _createCommentTheadWidget(owner: string, commentThread: languages.CommentThread) { - this._commentThreadWidget?.dispose(); - this.commentTheadDisposables.clear(); - this._commentThreadWidget = this.instantiationService.createInstance( + this._commentThreadDisposables.clear(); + this._commentThreadWidget.value = this.instantiationService.createInstance( CommentThreadWidget, this.container, this.notebookEditor, @@ -84,44 +79,48 @@ export class CellComments extends CellContentPart { const layoutInfo = this.notebookEditor.getLayoutInfo(); - await this._commentThreadWidget.display(layoutInfo.fontInfo.lineHeight, true); + await this._commentThreadWidget.value.display(layoutInfo.fontInfo.lineHeight, true); this._applyTheme(); - this.commentTheadDisposables.add(this._commentThreadWidget.onDidResize(() => { - if (this.currentElement?.cellKind === CellKind.Code && this._commentThreadWidget) { - this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.getDimensions().height); + this._commentThreadDisposables.add(this._commentThreadWidget.value.onDidResize(() => { + if (this.currentElement?.cellKind === CellKind.Code && this._commentThreadWidget.value) { + this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.value.getDimensions().height); } })); } private _bindListeners() { - this.cellDisposables.add(this.commentService.onDidUpdateCommentThreads(async () => { - if (this.currentElement) { - const info = await this._getCommentThreadForCell(this.currentElement); - if (!this._commentThreadWidget && info) { - await this._createCommentTheadWidget(info.owner, info.thread); - const layoutInfo = (this.currentElement as CodeCellViewModel).layoutInfo; - this.container.style.top = `${layoutInfo.outputContainerOffset + layoutInfo.outputTotalHeight}px`; - this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget!.getDimensions().height); - return; - } - - if (this._commentThreadWidget) { - if (!info) { - this._commentThreadWidget.dispose(); - this.currentElement.commentHeight = 0; - return; - } - if (this._commentThreadWidget.commentThread === info.thread) { - this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.getDimensions().height); - return; - } - - await this._commentThreadWidget.updateCommentThread(info.thread); - this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.getDimensions().height); - } + this.cellDisposables.add(this.commentService.onDidUpdateCommentThreads(async () => this._updateThread())); + } + + private async _updateThread() { + if (!this.currentElement) { + return; + } + const info = await this._getCommentThreadForCell(this.currentElement); + if (!this._commentThreadWidget.value && info) { + await this._createCommentTheadWidget(info.owner, info.thread); + const layoutInfo = (this.currentElement as CodeCellViewModel).layoutInfo; + this.container.style.top = `${layoutInfo.outputContainerOffset + layoutInfo.outputTotalHeight}px`; + this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.value!.getDimensions().height); + return; + } + + if (this._commentThreadWidget.value) { + if (!info) { + this._commentThreadDisposables.clear(); + this._commentThreadWidget.value = undefined; + this.currentElement.commentHeight = 0; + return; } - })); + if (this._commentThreadWidget.value.commentThread === info.thread) { + this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.value.getDimensions().height); + return; + } + + await this._commentThreadWidget.value.updateCommentThread(info.thread); + this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.value.getDimensions().height); + } } private _calculateCommentThreadHeight(bodyHeight: number) { @@ -151,12 +150,11 @@ export class CellComments extends CellContentPart { private _applyTheme() { const theme = this.themeService.getColorTheme(); const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; - this._commentThreadWidget?.applyTheme(theme, fontInfo); + this._commentThreadWidget.value?.applyTheme(theme, fontInfo); } override didRenderCell(element: ICellViewModel): void { if (element.cellKind === CellKind.Code) { - this.currentElement = element as CodeCellViewModel; this.initialize(element); this._bindListeners(); } @@ -164,13 +162,13 @@ export class CellComments extends CellContentPart { } override prepareLayout(): void { - if (this.currentElement?.cellKind === CellKind.Code && this._commentThreadWidget) { - this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.getDimensions().height); + if (this.currentElement?.cellKind === CellKind.Code && this._commentThreadWidget.value) { + this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.value.getDimensions().height); } } override updateInternalLayoutNow(element: ICellViewModel): void { - if (this.currentElement?.cellKind === CellKind.Code && this._commentThreadWidget) { + if (this.currentElement?.cellKind === CellKind.Code && this._commentThreadWidget.value) { const layoutInfo = (element as CodeCellViewModel).layoutInfo; this.container.style.top = `${layoutInfo.outputContainerOffset + layoutInfo.outputTotalHeight}px`; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts index 06444079e65..407fc5eaa81 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts @@ -335,7 +335,7 @@ export class CellDragAndDropController extends Disposable { const dragImage = dragImageProvider(); cellRoot.parentElement!.appendChild(dragImage); event.dataTransfer.setDragImage(dragImage, 0, 0); - setTimeout(() => cellRoot.parentElement!.removeChild(dragImage), 0); // Comment this out to debug drag image layout + setTimeout(() => dragImage.remove(), 0); // Comment this out to debug drag image layout }; for (const dragHandle of dragHandles) { templateData.templateDisposables.add(DOM.addDisposableListener(dragHandle, DOM.EventType.DRAG_START, onDragStart)); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts index 6cb5b68da50..f864ea9345d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts @@ -95,7 +95,7 @@ class CellOutputElement extends Disposable { } detach() { - this.renderedOutputContainer?.parentElement?.removeChild(this.renderedOutputContainer); + this.renderedOutputContainer?.remove(); let count = 0; if (this.innerContainer) { @@ -110,7 +110,7 @@ class CellOutputElement extends Disposable { } if (count === 0) { - this.innerContainer.parentElement?.removeChild(this.innerContainer); + this.innerContainer.remove(); } } @@ -154,7 +154,7 @@ class CellOutputElement extends Disposable { this._renderDisposableStore.clear(); const element = this.innerContainer; if (element) { - element.parentElement?.removeChild(element); + element.remove(); this.notebookEditor.removeInset(this.output); } @@ -400,7 +400,7 @@ class CellOutputElement extends Disposable { this._renderDisposableStore.clear(); const element = this.innerContainer; if (element) { - element.parentElement?.removeChild(element); + element.remove(); this.notebookEditor.removeInset(viewModel); } @@ -807,5 +807,3 @@ const JUPYTER_RENDERER_MIMETYPES = [ 'application/vnd.jupyter.widget-view+json', 'application/vnd.code.notebook.error' ]; - - diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts index 98b202b5b1c..a4514e41d4d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts @@ -31,7 +31,7 @@ import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/hover/ import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; -import type { IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; const $ = DOM.$; @@ -123,12 +123,15 @@ export class CellEditorStatusBar extends CellContentPart { override didRenderCell(element: ICellViewModel): void { - this.updateContext({ - ui: true, - cell: element, - notebookEditor: this._notebookEditor, - $mid: MarshalledId.NotebookCellActionContext - }); + if (this._notebookEditor.hasModel()) { + const context: (INotebookCellActionContext & { $mid: number }) = { + ui: true, + cell: element, + notebookEditor: this._notebookEditor, + $mid: MarshalledId.NotebookCellActionContext + }; + this.updateContext(context); + } if (this._editor) { // Focus Mode @@ -235,7 +238,7 @@ export class CellEditorStatusBar extends CellContentPart { if (renderedItems.length > newItems.length) { const deleted = renderedItems.splice(newItems.length, renderedItems.length - newItems.length); for (const deletedItem of deleted) { - container.removeChild(deletedItem.container); + deletedItem.container.remove(); deletedItem.dispose(); } } @@ -327,8 +330,8 @@ class CellStatusBarItem extends Disposable { this.container.setAttribute('role', role || ''); if (item.tooltip) { - const hoverContent = typeof item.tooltip === 'string' ? item.tooltip : { markdown: item.tooltip } as IUpdatableHoverTooltipMarkdownString; - this._itemDisposables.add(this._hoverService.setupUpdatableHover(this._hoverDelegate, this.container, hoverContent)); + const hoverContent = typeof item.tooltip === 'string' ? item.tooltip : { markdown: item.tooltip, markdownNotSupportedFallback: undefined } satisfies IManagedHoverTooltipMarkdownString; + this._itemDisposables.add(this._hoverService.setupManagedHover(this._hoverDelegate, this.container, hoverContent)); } this.container.classList.toggle('cell-status-item-has-command', !!item.command); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts index 733ac19d6bb..36911bfbdbb 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts @@ -80,13 +80,15 @@ export class BetweenCellToolbar extends CellOverlayPart { override didRenderCell(element: ICellViewModel): void { const betweenCellToolbar = this._initialize(); - betweenCellToolbar.context = { - ui: true, - cell: element, - notebookEditor: this._notebookEditor, - source: 'insertToolbar', - $mid: MarshalledId.NotebookCellActionContext - }; + if (this._notebookEditor.hasModel()) { + betweenCellToolbar.context = { + ui: true, + cell: element, + notebookEditor: this._notebookEditor, + source: 'insertToolbar', + $mid: MarshalledId.NotebookCellActionContext + } satisfies (INotebookCellActionContext & { source?: string; $mid: number }); + } this.updateInternalLayoutNow(element); } @@ -202,13 +204,17 @@ export class CellTitleToolbarPart extends CellOverlayPart { const view = this._initialize(model, element); this.cellDisposables.add(registerCellToolbarStickyScroll(this._notebookEditor, element, this.toolbarContainer, { extraOffset: 4, min: -14 })); - this.updateContext(view, { - ui: true, - cell: element, - notebookEditor: this._notebookEditor, - source: 'cellToolbar', - $mid: MarshalledId.NotebookCellActionContext - }); + if (this._notebookEditor.hasModel()) { + const toolbarContext: INotebookCellActionContext & { source?: string; $mid: number } = { + ui: true, + cell: element, + notebookEditor: this._notebookEditor, + source: 'cellToolbar', + $mid: MarshalledId.NotebookCellActionContext + }; + + this.updateContext(view, toolbarContext); + } } private updateContext(view: CellTitleToolbarView, toolbarContext: INotebookCellActionContext) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index 0e275775036..5472a771c8f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -129,7 +129,7 @@ export class CodeCell extends Disposable { const executionItemElement = DOM.append(this.templateData.cellInputCollapsedContainer, DOM.$('.collapsed-execution-icon')); this._register(toDisposable(() => { - executionItemElement.parentElement?.removeChild(executionItemElement); + executionItemElement.remove(); })); this._collapsedExecutionIcon = this._register(this.instantiationService.createInstance(CollapsedCodeCellExecutionIcon, this.notebookEditor, this.viewCell, executionItemElement)); this.updateForCollapseState(); @@ -497,7 +497,7 @@ export class CodeCell extends Disposable { } elements.forEach(element => { - element.parentElement?.removeChild(element); + element.remove(); }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts index de2c0e912bf..c6f345362ea 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts @@ -58,12 +58,15 @@ export class RunToolbar extends CellContentPart { override didRenderCell(element: ICellViewModel): void { this.cellDisposables.add(registerCellToolbarStickyScroll(this.notebookEditor, element, this.runButtonContainer)); - this.toolbar.context = { - ui: true, - cell: element, - notebookEditor: this.notebookEditor, - $mid: MarshalledId.NotebookCellActionContext - }; + if (this.notebookEditor.hasModel()) { + const context: INotebookCellActionContext & { $mid: number } = { + ui: true, + cell: element, + notebookEditor: this.notebookEditor, + $mid: MarshalledId.NotebookCellActionContext + }; + this.toolbar.context = context; + } } getCellToolbarActions(menu: IMenu): { primary: IAction[]; secondary: IAction[] } { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts index b1a3e8b3e93..7fbe705a0a0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts @@ -349,7 +349,7 @@ export class MarkupCell extends Disposable { // create a special context key service that set the inCompositeEditor-contextkey const editorContextKeyService = this.contextKeyService.createScoped(this.templateData.editorPart); EditorContextKeys.inCompositeEditor.bindTo(editorContextKeyService).set(true); - const editorInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editorContextKeyService])); + const editorInstaService = this.editorDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editorContextKeyService]))); this.editorDisposables.add(editorContextKeyService); this.editor = this.editorDisposables.add(editorInstaService.createInstance(CodeEditorWidget, this.templateData.editorContainer, { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 9cc82bc223a..51c11f43383 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -30,7 +30,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { ITextEditorOptions, ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { IFileService } from 'vs/platform/files/common/files'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -270,7 +270,7 @@ export class BackLayerWebView extends Themable { 'notebook-markdown-line-height': typeof this.options.markdownLineHeight === 'number' && this.options.markdownLineHeight > 0 ? `${this.options.markdownLineHeight}px` : `normal`, 'notebook-cell-output-font-size': `${this.options.outputFontSize || this.options.fontSize}px`, 'notebook-cell-output-line-height': `${this.options.outputLineHeight}px`, - 'notebook-cell-output-max-height': `${this.options.outputLineHeight * this.options.outputLineLimit}px`, + 'notebook-cell-output-max-height': `${this.options.outputLineHeight * this.options.outputLineLimit + 2}px`, 'notebook-cell-output-font-family': this.options.outputFontFamily || this.options.fontFamily, 'notebook-cell-markup-empty-content': nls.localize('notebook.emptyMarkdownPlaceholder', "Empty markdown cell, double-click or press enter to edit."), 'notebook-cell-renderer-not-found-error': nls.localize({ @@ -436,7 +436,7 @@ export class BackLayerWebView extends Themable { } table, thead, tr, th, td, tbody { - border: none !important; + border: none; border-color: transparent; border-spacing: 0; border-collapse: collapse; @@ -1119,7 +1119,9 @@ export class BackLayerWebView extends Themable { } if (match) { - match.group.openEditor(match.editor, lineNumber !== undefined && column !== undefined ? { selection: { startLineNumber: lineNumber, startColumn: column } } : undefined); + const selection: ITextEditorSelection | undefined = lineNumber !== undefined && column !== undefined ? { startLineNumber: lineNumber, startColumn: column } : undefined; + const textEditorOptions: ITextEditorOptions = { selection: selection }; + match.group.openEditor(match.editor, selection ? textEditorOptions : undefined); } else { this.openerService.open(uri, { fromUserGesture: true, fromWorkspace: true }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 9f6f5b8dac5..564ddd06414 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -157,7 +157,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen const innerContent = DOM.append(container, $('.cell.markdown')); const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); - const scopedInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])); + const scopedInstaService = templateDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService]))); const rootClassDelegate = { toggle: (className: string, force?: boolean) => container.classList.toggle(className, force) }; @@ -279,7 +279,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende // create a special context key service that set the inCompositeEditor-contextkey const editorContextKeyService = templateDisposables.add(this.contextKeyServiceProvider(editorPart)); - const editorInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editorContextKeyService])); + const editorInstaService = templateDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editorContextKeyService]))); EditorContextKeys.inCompositeEditor.bindTo(editorContextKeyService).set(true); const editor = editorInstaService.createInstance(CodeEditorWidget, editorContainer, { @@ -303,7 +303,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const bottomCellToolbarContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); const focusIndicatorBottom = new FastDomNode(DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-bottom'))); - const scopedInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])); + const scopedInstaService = templateDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService]))); const rootClassDelegate = { toggle: (className: string, force?: boolean) => container.classList.toggle(className, force) }; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index fb7dffded14..7f79531b37e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -3069,7 +3069,7 @@ async function webviewPreloads(ctx: PreloadContext) { }); if (this.dragOverlay) { - window.document.body.removeChild(this.dragOverlay); + this.dragOverlay.remove(); this.dragOverlay = undefined; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index f946c2142d9..b0751fc5b96 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -9,7 +9,7 @@ import { Mimes } from 'vs/base/common/mime'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IPosition } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; @@ -24,7 +24,7 @@ import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browse import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, INotebookCellStatusBarItem, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, INotebookCellStatusBarItem, INotebookFindOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export abstract class BaseCellViewModel extends Disposable { @@ -258,7 +258,12 @@ export abstract class BaseCellViewModel extends Disposable { writeTransientState(editor.getModel(), this._editorTransientState, this._codeEditorService); } - this._textEditor?.changeDecorations((accessor) => { + if (this._isDisposed) { + // Restore View State could adjust the editor layout and trigger a list view update. The list view update might then dispose this view model. + return; + } + + editor.changeDecorations((accessor) => { this._resolvedDecorations.forEach((value, key) => { if (key.startsWith('_lazy_')) { // lazy ones @@ -272,7 +277,7 @@ export abstract class BaseCellViewModel extends Disposable { }); }); - this._editorListeners.push(this._textEditor.onDidChangeCursorSelection(() => { this._onDidChangeState.fire({ selectionChanged: true }); })); + this._editorListeners.push(editor.onDidChangeCursorSelection(() => { this._onDidChangeState.fire({ selectionChanged: true }); })); const inlineChatController = InlineChatController.get(this._textEditor); if (inlineChatController) { this._editorListeners.push(inlineChatController.onWillStartSession(() => { @@ -281,7 +286,7 @@ export abstract class BaseCellViewModel extends Disposable { } })); } - // this._editorListeners.push(this._textEditor.onKeyDown(e => this.handleKeyDown(e))); + this._onDidChangeState.fire({ selectionChanged: true }); this._onDidChangeEditorAttachState.fire(); } @@ -645,20 +650,21 @@ export abstract class BaseCellViewModel extends Disposable { protected abstract onDidChangeTextModelContent(): void; - protected cellStartFind(value: string, options: INotebookSearchOptions): model.FindMatch[] | null { + protected cellStartFind(value: string, options: INotebookFindOptions): model.FindMatch[] | null { let cellMatches: model.FindMatch[] = []; + const lineCount = this.textBuffer.getLineCount(); + const findRange: IRange[] = options.findScope?.selectedTextRanges ?? [new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1)]; + if (this.assertTextModelAttached()) { cellMatches = this.textModel!.findMatches( value, - false, + findRange, options.regex || false, options.caseSensitive || false, options.wholeWord ? options.wordSeparators || null : null, options.regex || false); } else { - const lineCount = this.textBuffer.getLineCount(); - const fullRange = new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1); const searchParams = new SearchParams(value, options.regex || false, options.caseSensitive || false, options.wholeWord ? options.wordSeparators || null : null,); const searchData = searchParams.parseSearchRequest(); @@ -666,7 +672,9 @@ export abstract class BaseCellViewModel extends Disposable { return null; } - cellMatches = this.textBuffer.findMatchesLineByLine(fullRange, searchData, options.regex || false, 1000); + findRange.forEach(range => { + cellMatches.push(...this.textBuffer.findMatchesLineByLine(new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn), searchData, options.regex || false, 1000)); + }); } return cellMatches; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 3f5a6c3e68d..06c8c851f3b 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -5,25 +5,25 @@ import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event'; import { dispose } from 'vs/base/common/lifecycle'; +import { observableValue } from 'vs/base/common/observable'; import * as UUID from 'vs/base/common/uuid'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { PrefixSumComputer } from 'vs/editor/common/model/prefixSumComputer'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { CellEditState, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, CellLayoutState, ICellOutputViewModel, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFindMatch, CellLayoutState, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, ICellOutputViewModel, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; +import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { CellOutputViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/cellOutputViewModel'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, INotebookSearchOptions, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; +import { CellKind, INotebookFindOptions, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICellExecutionError, ICellExecutionStateChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { BaseCellViewModel } from './baseCellViewModel'; -import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { ICellExecutionError, ICellExecutionStateChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { observableValue } from 'vs/base/common/observable'; export const outputDisplayLimit = 500; @@ -518,7 +518,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod private readonly _hasFindResult = this._register(new Emitter()); public readonly hasFindResult: Event = this._hasFindResult.event; - startFind(value: string, options: INotebookSearchOptions): CellFindMatch | null { + startFind(value: string, options: INotebookFindOptions): CellFindMatch | null { const matches = super.cellStartFind(value, options); if (matches === null) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts index 0f255cc20db..ee1b0d37d9f 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts @@ -10,7 +10,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { CellEditState, CellFindMatch, CellFoldingState, CellLayoutContext, CellLayoutState, EditorFoldingStateDelegate, ICellOutputViewModel, ICellViewModel, MarkupCellLayoutChangeEvent, MarkupCellLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, INotebookFindOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; @@ -295,7 +295,7 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM private readonly _hasFindResult = this._register(new Emitter()); public readonly hasFindResult: Event = this._hasFindResult.event; - startFind(value: string, options: INotebookSearchOptions): CellFindMatch | null { + startFind(value: string, options: INotebookFindOptions): CellFindMatch | null { const matches = super.cellStartFind(value, options); if (matches === null) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts new file mode 100644 index 00000000000..767b9f8f8de --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts @@ -0,0 +1,222 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/event'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { IActiveNotebookEditor, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { OutlineChangeEvent, OutlineConfigKeys } from 'vs/workbench/services/outline/browser/outline'; +import { OutlineEntry } from './OutlineEntry'; +import { IOutlineModelService } from 'vs/editor/contrib/documentSymbols/browser/outlineModel'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory'; + +export interface INotebookCellOutlineDataSource { + readonly activeElement: OutlineEntry | undefined; + readonly entries: OutlineEntry[]; +} + +export class NotebookCellOutlineDataSource implements INotebookCellOutlineDataSource { + + private readonly _disposables = new DisposableStore(); + + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + private _uri: URI | undefined; + private _entries: OutlineEntry[] = []; + private _activeEntry?: OutlineEntry; + + private readonly _outlineEntryFactory: NotebookOutlineEntryFactory; + + constructor( + private readonly _editor: INotebookEditor, + @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, + @IOutlineModelService private readonly _outlineModelService: IOutlineModelService, + @IMarkerService private readonly _markerService: IMarkerService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + this._outlineEntryFactory = new NotebookOutlineEntryFactory(this._notebookExecutionStateService); + this.recomputeState(); + } + + get activeElement(): OutlineEntry | undefined { + return this._activeEntry; + } + get entries(): OutlineEntry[] { + return this._entries; + } + get isEmpty(): boolean { + return this._entries.length === 0; + } + get uri() { + return this._uri; + } + + public async computeFullSymbols(cancelToken: CancellationToken) { + const notebookEditorWidget = this._editor; + + const notebookCells = notebookEditorWidget?.getViewModel()?.viewCells.filter((cell) => cell.cellKind === CellKind.Code); + + if (notebookCells) { + const promises: Promise[] = []; + // limit the number of cells so that we don't resolve an excessive amount of text models + for (const cell of notebookCells.slice(0, 100)) { + // gather all symbols asynchronously + promises.push(this._outlineEntryFactory.cacheSymbols(cell, this._outlineModelService, cancelToken)); + } + await Promise.allSettled(promises); + } + this.recomputeState(); + } + + public recomputeState(): void { + this._disposables.clear(); + this._activeEntry = undefined; + this._uri = undefined; + + if (!this._editor.hasModel()) { + return; + } + + this._uri = this._editor.textModel.uri; + + const notebookEditorWidget: IActiveNotebookEditor = this._editor; + + if (notebookEditorWidget.getLength() === 0) { + return; + } + + const notebookCells = notebookEditorWidget.getViewModel().viewCells; + + const entries: OutlineEntry[] = []; + for (const cell of notebookCells) { + entries.push(...this._outlineEntryFactory.getOutlineEntries(cell, entries.length)); + } + + // build a tree from the list of entries + if (entries.length > 0) { + const result: OutlineEntry[] = [entries[0]]; + const parentStack: OutlineEntry[] = [entries[0]]; + + for (let i = 1; i < entries.length; i++) { + const entry = entries[i]; + + while (true) { + const len = parentStack.length; + if (len === 0) { + // root node + result.push(entry); + parentStack.push(entry); + break; + + } else { + const parentCandidate = parentStack[len - 1]; + if (parentCandidate.level < entry.level) { + parentCandidate.addChild(entry); + parentStack.push(entry); + break; + } else { + parentStack.pop(); + } + } + } + } + this._entries = result; + } + + // feature: show markers with each cell + const markerServiceListener = new MutableDisposable(); + this._disposables.add(markerServiceListener); + const updateMarkerUpdater = () => { + if (notebookEditorWidget.isDisposed) { + return; + } + + const doUpdateMarker = (clear: boolean) => { + for (const entry of this._entries) { + if (clear) { + entry.clearMarkers(); + } else { + entry.updateMarkers(this._markerService); + } + } + }; + const problem = this._configurationService.getValue('problems.visibility'); + if (problem === undefined) { + return; + } + + const config = this._configurationService.getValue(OutlineConfigKeys.problemsEnabled); + + if (problem && config) { + markerServiceListener.value = this._markerService.onMarkerChanged(e => { + if (notebookEditorWidget.isDisposed) { + console.error('notebook editor is disposed'); + return; + } + + if (e.some(uri => notebookEditorWidget.getCellsInRange().some(cell => isEqual(cell.uri, uri)))) { + doUpdateMarker(false); + this._onDidChange.fire({}); + } + }); + doUpdateMarker(false); + } else { + markerServiceListener.clear(); + doUpdateMarker(true); + } + }; + updateMarkerUpdater(); + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('problems.visibility') || e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) { + updateMarkerUpdater(); + this._onDidChange.fire({}); + } + })); + + const { changeEventTriggered } = this.recomputeActive(); + if (!changeEventTriggered) { + this._onDidChange.fire({}); + } + } + + public recomputeActive(): { changeEventTriggered: boolean } { + let newActive: OutlineEntry | undefined; + const notebookEditorWidget = this._editor; + + if (notebookEditorWidget) {//TODO don't check for widget, only here if we do have + if (notebookEditorWidget.hasModel() && notebookEditorWidget.getLength() > 0) { + const cell = notebookEditorWidget.cellAt(notebookEditorWidget.getFocus().start); + if (cell) { + for (const entry of this._entries) { + newActive = entry.find(cell, []); + if (newActive) { + break; + } + } + } + } + } + + if (newActive !== this._activeEntry) { + this._activeEntry = newActive; + this._onDidChange.fire({ affectOnlyActiveElement: true }); + return { changeEventTriggered: true }; + } + return { changeEventTriggered: false }; + } + + dispose(): void { + this._entries.length = 0; + this._activeEntry = undefined; + this._disposables.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory.ts new file mode 100644 index 00000000000..c4d2f82593a --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ReferenceCollection, type IReference } from 'vs/base/common/lifecycle'; +import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import type { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookCellOutlineDataSource } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource'; + +class NotebookCellOutlineDataSourceReferenceCollection extends ReferenceCollection { + constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { + super(); + } + protected override createReferencedObject(_key: string, editor: INotebookEditor): NotebookCellOutlineDataSource { + return this.instantiationService.createInstance(NotebookCellOutlineDataSource, editor); + } + protected override destroyReferencedObject(_key: string, object: NotebookCellOutlineDataSource): void { + object.dispose(); + } +} + +export const INotebookCellOutlineDataSourceFactory = createDecorator('INotebookCellOutlineDataSourceFactory'); + +export interface INotebookCellOutlineDataSourceFactory { + getOrCreate(editor: INotebookEditor): IReference; +} + +export class NotebookCellOutlineDataSourceFactory implements INotebookCellOutlineDataSourceFactory { + private readonly _data: NotebookCellOutlineDataSourceReferenceCollection; + constructor(@IInstantiationService instantiationService: IInstantiationService) { + this._data = instantiationService.createInstance(NotebookCellOutlineDataSourceReferenceCollection); + } + + getOrCreate(editor: INotebookEditor): IReference { + return this._data.acquire(editor.getId(), editor); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts index 77a8ba22ab4..a7de8857584 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts @@ -14,7 +14,6 @@ import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { IRange } from 'vs/editor/common/core/range'; import { SymbolKind } from 'vs/editor/common/languages'; -import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; export const enum NotebookOutlineConstants { NonHeaderOutlineLevel = 7, @@ -50,7 +49,7 @@ export class NotebookOutlineEntryFactory { private readonly executionStateService: INotebookExecutionStateService ) { } - public getOutlineEntries(cell: ICellViewModel, target: OutlineTarget, index: number): OutlineEntry[] { + public getOutlineEntries(cell: ICellViewModel, index: number): OutlineEntry[] { const entries: OutlineEntry[] = []; const isMarkdown = cell.cellKind === CellKind.Markup; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts deleted file mode 100644 index 1ac843597ab..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts +++ /dev/null @@ -1,316 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; -import { isEqual } from 'vs/base/common/resources'; -import { URI } from 'vs/base/common/uri'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IMarkerService } from 'vs/platform/markers/common/markers'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IActiveNotebookEditor, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind, NotebookCellsChangeType, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { OutlineChangeEvent, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; -import { OutlineEntry } from './OutlineEntry'; -import { IOutlineModelService } from 'vs/editor/contrib/documentSymbols/browser/outlineModel'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { NotebookOutlineConstants, NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory'; -import { Delayer } from 'vs/base/common/async'; - -export class NotebookCellOutlineProvider { - private readonly _disposables = new DisposableStore(); - private readonly _onDidChange = new Emitter(); - - readonly onDidChange: Event = this._onDidChange.event; - - private _uri: URI | undefined; - private _entries: OutlineEntry[] = []; - get entries(): OutlineEntry[] { - if (this.delayedOutlineRecompute.isTriggered()) { - this.delayedOutlineRecompute.cancel(); - this._recomputeState(); - } - return this._entries; - } - - private _activeEntry?: OutlineEntry; - private readonly _entriesDisposables = new DisposableStore(); - - readonly outlineKind = 'notebookCells'; - - get activeElement(): OutlineEntry | undefined { - if (this.delayedOutlineRecompute.isTriggered()) { - this.delayedOutlineRecompute.cancel(); - this._recomputeState(); - } - return this._activeEntry; - } - - private readonly _outlineEntryFactory: NotebookOutlineEntryFactory; - private readonly delayedOutlineRecompute: Delayer; - constructor( - private readonly _editor: INotebookEditor, - private readonly _target: OutlineTarget, - @IThemeService themeService: IThemeService, - @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, - @IOutlineModelService private readonly _outlineModelService: IOutlineModelService, - @IMarkerService private readonly _markerService: IMarkerService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - ) { - this._outlineEntryFactory = new NotebookOutlineEntryFactory(notebookExecutionStateService); - - const delayerRecomputeActive = this._disposables.add(new Delayer(200)); - this._disposables.add(_editor.onDidChangeSelection(() => { - delayerRecomputeActive.trigger(() => this._recomputeActive()); - }, this)); - - // .3s of a delay is sufficient, 100-200s is too quick and will unnecessarily block the ui thread. - // Given we're only updating the outline when the user types, we can afford to wait a bit. - this.delayedOutlineRecompute = this._disposables.add(new Delayer(300)); - const delayedRecompute = () => { - delayerRecomputeActive.cancel(); // Active is always recomputed after a recomputing the outline state. - this.delayedOutlineRecompute.trigger(() => this._recomputeState()); - }; - - this._disposables.add(_configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(NotebookSetting.outlineShowMarkdownHeadersOnly) || - e.affectsConfiguration(NotebookSetting.outlineShowCodeCells) || - e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols) || - e.affectsConfiguration(NotebookSetting.breadcrumbsShowCodeCells) - ) { - delayedRecompute(); - } - })); - - this._disposables.add(themeService.onDidFileIconThemeChange(() => { - this._onDidChange.fire({}); - })); - - this._disposables.add( - notebookExecutionStateService.onDidChangeExecution(e => { - if (e.type === NotebookExecutionType.cell && !!this._editor.textModel && e.affectsNotebook(this._editor.textModel?.uri)) { - delayedRecompute(); - } - }) - ); - - const disposable = this._disposables.add(new DisposableStore()); - const monitorModelChanges = () => { - disposable.clear(); - if (!this._editor.textModel) { - return; - } - disposable.add(this._editor.textModel.onDidChangeContent(contentChanges => { - if (contentChanges.rawEvents.some(c => c.kind === NotebookCellsChangeType.ChangeCellContent || - c.kind === NotebookCellsChangeType.ChangeCellInternalMetadata || - c.kind === NotebookCellsChangeType.Move || - c.kind === NotebookCellsChangeType.ModelChange)) { - delayedRecompute(); - } - })); - // Perhaps this is the first time we're building the outline - if (!this._entries.length) { - this._recomputeState(); - } - }; - this._disposables.add(this._editor.onDidChangeModel(monitorModelChanges)); - monitorModelChanges(); - this._recomputeState(); - } - - dispose(): void { - this._entries.length = 0; - this._activeEntry = undefined; - this._entriesDisposables.dispose(); - this._disposables.dispose(); - } - - async setFullSymbols(cancelToken: CancellationToken) { - const notebookEditorWidget = this._editor; - - const notebookCells = notebookEditorWidget?.getViewModel()?.viewCells.filter((cell) => cell.cellKind === CellKind.Code); - - if (notebookCells) { - const promises: Promise[] = []; - // limit the number of cells so that we don't resolve an excessive amount of text models - for (const cell of notebookCells.slice(0, 100)) { - // gather all symbols asynchronously - promises.push(this._outlineEntryFactory.cacheSymbols(cell, this._outlineModelService, cancelToken)); - } - await Promise.allSettled(promises); - } - - this._recomputeState(); - } - private _recomputeState(): void { - this._entriesDisposables.clear(); - this._activeEntry = undefined; - this._uri = undefined; - - if (!this._editor.hasModel()) { - return; - } - - this._uri = this._editor.textModel.uri; - - const notebookEditorWidget: IActiveNotebookEditor = this._editor; - - if (notebookEditorWidget.getLength() === 0) { - return; - } - - let includeCodeCells = true; - if (this._target === OutlineTarget.Breadcrumbs) { - includeCodeCells = this._configurationService.getValue('notebook.breadcrumbs.showCodeCells'); - } - - let notebookCells: ICellViewModel[]; - if (this._target === OutlineTarget.Breadcrumbs) { - notebookCells = notebookEditorWidget.getViewModel().viewCells.filter((cell) => cell.cellKind === CellKind.Markup || includeCodeCells); - } else { - notebookCells = notebookEditorWidget.getViewModel().viewCells; - } - - const entries: OutlineEntry[] = []; - for (const cell of notebookCells) { - entries.push(...this._outlineEntryFactory.getOutlineEntries(cell, this._target, entries.length)); - } - - // build a tree from the list of entries - if (entries.length > 0) { - const result: OutlineEntry[] = [entries[0]]; - const parentStack: OutlineEntry[] = [entries[0]]; - - for (let i = 1; i < entries.length; i++) { - const entry = entries[i]; - - while (true) { - const len = parentStack.length; - if (len === 0) { - // root node - result.push(entry); - parentStack.push(entry); - break; - - } else { - const parentCandidate = parentStack[len - 1]; - if (parentCandidate.level < entry.level) { - parentCandidate.addChild(entry); - parentStack.push(entry); - break; - } else { - parentStack.pop(); - } - } - } - } - this._entries = result; - } - - // feature: show markers with each cell - const markerServiceListener = new MutableDisposable(); - this._entriesDisposables.add(markerServiceListener); - const updateMarkerUpdater = () => { - if (notebookEditorWidget.isDisposed) { - return; - } - - const doUpdateMarker = (clear: boolean) => { - for (const entry of this._entries) { - if (clear) { - entry.clearMarkers(); - } else { - entry.updateMarkers(this._markerService); - } - } - }; - const problem = this._configurationService.getValue('problems.visibility'); - if (problem === undefined) { - return; - } - - const config = this._configurationService.getValue(OutlineConfigKeys.problemsEnabled); - - if (problem && config) { - markerServiceListener.value = this._markerService.onMarkerChanged(e => { - if (notebookEditorWidget.isDisposed) { - console.error('notebook editor is disposed'); - return; - } - - if (e.some(uri => notebookEditorWidget.getCellsInRange().some(cell => isEqual(cell.uri, uri)))) { - doUpdateMarker(false); - this._onDidChange.fire({}); - } - }); - doUpdateMarker(false); - } else { - markerServiceListener.clear(); - doUpdateMarker(true); - } - }; - updateMarkerUpdater(); - this._entriesDisposables.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('problems.visibility') || e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) { - updateMarkerUpdater(); - this._onDidChange.fire({}); - } - })); - - const { changeEventTriggered } = this._recomputeActive(); - if (!changeEventTriggered) { - this._onDidChange.fire({}); - } - } - - private _recomputeActive(): { changeEventTriggered: boolean } { - let newActive: OutlineEntry | undefined; - const notebookEditorWidget = this._editor; - - if (notebookEditorWidget) {//TODO don't check for widget, only here if we do have - if (notebookEditorWidget.hasModel() && notebookEditorWidget.getLength() > 0) { - const cell = notebookEditorWidget.cellAt(notebookEditorWidget.getFocus().start); - if (cell) { - for (const entry of this._entries) { - newActive = entry.find(cell, []); - if (newActive) { - break; - } - } - } - } - } - - // @Yoyokrazy - Make sure the new active entry isn't part of the filtered exclusions - const showCodeCells = this._configurationService.getValue(NotebookSetting.outlineShowCodeCells); - const showCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); - const showMarkdownHeadersOnly = this._configurationService.getValue(NotebookSetting.outlineShowMarkdownHeadersOnly); - - // check the three outline filtering conditions - // if any are true, newActive should NOT be set to this._activeEntry and the event should NOT fire - if ( - (newActive !== this._activeEntry) && !( - (showMarkdownHeadersOnly && newActive?.cell.cellKind === CellKind.Markup && newActive?.level === NotebookOutlineConstants.NonHeaderOutlineLevel) || // show headers only + cell is mkdn + is level 7 (no header) - (!showCodeCells && newActive?.cell.cellKind === CellKind.Code) || // show code cells + cell is code - (!showCodeCellSymbols && newActive?.cell.cellKind === CellKind.Code && newActive?.level > NotebookOutlineConstants.NonHeaderOutlineLevel) // show code symbols + cell is code + has level > 7 (nb symbol levels) - ) - ) { - this._activeEntry = newActive; - this._onDidChange.fire({ affectOnlyActiveElement: true }); - return { changeEventTriggered: true }; - } - - return { changeEventTriggered: false }; - } - - get isEmpty(): boolean { - return this._entries.length === 0; - } - - get uri() { - return this._uri; - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory.ts deleted file mode 100644 index 54411bcd296..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory.ts +++ /dev/null @@ -1,39 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ReferenceCollection, type IReference } from 'vs/base/common/lifecycle'; -import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import type { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookCellOutlineProvider } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; -import type { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; - -class NotebookCellOutlineProviderReferenceCollection extends ReferenceCollection { - constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { - super(); - } - protected override createReferencedObject(_key: string, editor: INotebookEditor, target: OutlineTarget): NotebookCellOutlineProvider { - return this.instantiationService.createInstance(NotebookCellOutlineProvider, editor, target); - } - protected override destroyReferencedObject(_key: string, object: NotebookCellOutlineProvider): void { - object.dispose(); - } -} - -export const INotebookCellOutlineProviderFactory = createDecorator('INotebookCellOutlineProviderFactory'); - -export interface INotebookCellOutlineProviderFactory { - getOrCreate(editor: INotebookEditor, target: OutlineTarget): IReference; -} - -export class NotebookCellOutlineProviderFactory implements INotebookCellOutlineProviderFactory { - private readonly _data: NotebookCellOutlineProviderReferenceCollection; - constructor(@IInstantiationService instantiationService: IInstantiationService) { - this._data = instantiationService.createInstance(NotebookCellOutlineProviderReferenceCollection); - } - - getOrCreate(editor: INotebookEditor, target: OutlineTarget): IReference { - return this._data.acquire(editor.getId(), editor, target); - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index 8a0f8379011..60a3626484f 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -13,27 +13,27 @@ import { URI } from 'vs/base/common/uri'; import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IWorkspaceTextEdit } from 'vs/editor/common/languages'; import { FindMatch, IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { MultiModelEditStackElement, SingleModelEditStackElement } from 'vs/editor/common/model/editStack'; import { IntervalNode, IntervalTree } from 'vs/editor/common/model/intervalTree'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { IWorkspaceTextEdit } from 'vs/editor/common/languages'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { FoldingRegions } from 'vs/editor/contrib/folding/browser/foldingRanges'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { CellEditState, CellFindMatchWithIndex, CellFoldingState, EditorFoldingStateDelegate, ICellViewModel, INotebookDeltaCellStatusBarItems, INotebookDeltaDecoration, ICellModelDecorations, ICellModelDeltaDecorations, IModelDecorationsChangeAccessor, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellFindMatchModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; +import { CellEditState, CellFindMatchWithIndex, CellFoldingState, EditorFoldingStateDelegate, ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, IModelDecorationsChangeAccessor, INotebookDeltaCellStatusBarItems, INotebookDeltaDecoration, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookLayoutInfo, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { NotebookCellSelectionCollection } from 'vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, ICell, INotebookSearchOptions, ISelectionState, NotebookCellsChangeType, NotebookCellTextModelSplice, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { cellIndexesToRanges, cellRangesToIndexes, ICellRange, reduceCellRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { NotebookLayoutInfo, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { CellFindMatchModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; +import { CellKind, ICell, INotebookFindOptions, ISelectionState, NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookFindScopeType, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { cellIndexesToRanges, cellRangesToIndexes, ICellRange, reduceCellRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; const invalidFunc = () => { throw new Error(`Invalid change accessor`); }; @@ -99,6 +99,7 @@ let MODEL_ID = 0; export interface NotebookViewModelOptions { isReadOnly: boolean; + inRepl?: boolean; } export class NotebookViewModel extends Disposable implements EditorFoldingStateDelegate, INotebookViewModel { @@ -108,15 +109,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD private readonly _onDidChangeOptions = this._register(new Emitter()); get onDidChangeOptions(): Event { return this._onDidChangeOptions.event; } private _viewCells: CellViewModel[] = []; + private readonly replView: boolean; get viewCells(): ICellViewModel[] { return this._viewCells; } - set viewCells(_: ICellViewModel[]) { - throw new Error('NotebookViewModel.viewCells is readonly'); - } - get length(): number { return this._viewCells.length; } @@ -206,6 +204,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD MODEL_ID++; this.id = '$notebookViewModel' + MODEL_ID; this._instanceId = strings.singleLetterHash(MODEL_ID); + this.replView = !!this.options.inRepl; const compute = (changes: NotebookCellTextModelSplice[], synchronous: boolean) => { const diffs = changes.map(splice => { @@ -337,9 +336,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._onDidChangeSelection.fire(e); })); - this._viewCells = this._notebook.cells.map(cell => { - return createCellViewModel(this._instantiationService, this, cell, this._viewContext); - }); + + const viewCellCount = this.replView ? this._notebook.cells.length - 1 : this._notebook.cells.length; + for (let i = 0; i < viewCellCount; i++) { + this._viewCells.push(createCellViewModel(this._instantiationService, this, this._notebook.cells[i], this._viewContext)); + } + this._viewCells.forEach(cell => { this._handleToViewCellMapping.set(cell.handle, cell); @@ -908,13 +910,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } //#region Find - find(value: string, options: INotebookSearchOptions): CellFindMatchWithIndex[] { + find(value: string, options: INotebookFindOptions): CellFindMatchWithIndex[] { const matches: CellFindMatchWithIndex[] = []; let findCells: CellViewModel[] = []; - const selectedRanges = options.selectedRanges?.map(range => this.validateRange(range)).filter(range => !!range); - - if (options.searchInRanges && selectedRanges) { + if (options.findScope && (options.findScope.findScopeType === NotebookFindScopeType.Cells || options.findScope.findScopeType === NotebookFindScopeType.Text)) { + const selectedRanges = options.findScope.selectedCellRanges?.map(range => this.validateRange(range)).filter(range => !!range) ?? []; const selectedIndexes = cellRangesToIndexes(selectedRanges); findCells = selectedIndexes.map(index => this._viewCells[index]); } else { diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts index 52e6889c4bc..50a6758941b 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts @@ -13,7 +13,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { CellFoldingState, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/OutlineEntry'; -import { NotebookCellOutlineProvider } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; +import { NotebookCellOutlineDataSource } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Delayer } from 'vs/base/common/async'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -23,8 +23,7 @@ import { FoldingController } from 'vs/workbench/contrib/notebook/browser/control import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; import { NotebookSectionArgs } from 'vs/workbench/contrib/notebook/browser/controller/sectionActions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { INotebookCellOutlineProviderFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory'; -import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; +import { INotebookCellOutlineDataSourceFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory'; export class NotebookStickyLine extends Disposable { constructor( @@ -105,7 +104,7 @@ export class NotebookStickyScroll extends Disposable { private readonly _onDidChangeNotebookStickyScroll = this._register(new Emitter()); readonly onDidChangeNotebookStickyScroll: Event = this._onDidChangeNotebookStickyScroll.event; - private notebookOutlineReference?: IReference; + private notebookCellOutlineReference?: IReference; getDomNode(): HTMLElement { return this.domNode; @@ -191,37 +190,37 @@ export class NotebookStickyScroll extends Disposable { this.init(); } else { this._disposables.clear(); - this.notebookOutlineReference?.dispose(); + this.notebookCellOutlineReference?.dispose(); this.disposeCurrentStickyLines(); DOM.clearNode(this.domNode); this.updateDisplay(); } - } else if (e.stickyScrollMode && this.notebookEditor.notebookOptions.getDisplayOptions().stickyScrollEnabled && this.notebookOutlineReference?.object) { - this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, this.notebookOutlineReference?.object?.entries, this.getCurrentStickyHeight())); + } else if (e.stickyScrollMode && this.notebookEditor.notebookOptions.getDisplayOptions().stickyScrollEnabled && this.notebookCellOutlineReference?.object) { + this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, this.notebookCellOutlineReference?.object?.entries, this.getCurrentStickyHeight())); } } private init() { - const { object: notebookOutlineReference } = this.notebookOutlineReference = this.instantiationService.invokeFunction((accessor) => accessor.get(INotebookCellOutlineProviderFactory).getOrCreate(this.notebookEditor, OutlineTarget.OutlinePane)); - this._register(this.notebookOutlineReference); - this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, notebookOutlineReference.entries, this.getCurrentStickyHeight())); + const { object: notebookCellOutline } = this.notebookCellOutlineReference = this.instantiationService.invokeFunction((accessor) => accessor.get(INotebookCellOutlineDataSourceFactory).getOrCreate(this.notebookEditor)); + this._register(this.notebookCellOutlineReference); + this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight())); - this._disposables.add(notebookOutlineReference.onDidChange(() => { - const recompute = computeContent(this.notebookEditor, this.notebookCellList, notebookOutlineReference.entries, this.getCurrentStickyHeight()); + this._disposables.add(notebookCellOutline.onDidChange(() => { + const recompute = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); if (!this.compareStickyLineMaps(recompute, this.currentStickyLines)) { this.updateContent(recompute); } })); this._disposables.add(this.notebookEditor.onDidAttachViewModel(() => { - this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, notebookOutlineReference.entries, this.getCurrentStickyHeight())); + this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight())); })); this._disposables.add(this.notebookEditor.onDidScroll(() => { const d = new Delayer(100); d.trigger(() => { d.dispose(); - const recompute = computeContent(this.notebookEditor, this.notebookCellList, notebookOutlineReference.entries, this.getCurrentStickyHeight()); + const recompute = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); if (!this.compareStickyLineMaps(recompute, this.currentStickyLines)) { this.updateContent(recompute); } @@ -369,7 +368,7 @@ export class NotebookStickyScroll extends Disposable { override dispose() { this._disposables.dispose(); this.disposeCurrentStickyLines(); - this.notebookOutlineReference?.dispose(); + this.notebookCellOutlineReference?.dispose(); super.dispose(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts index 6348c0d359f..ea1039d672c 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts @@ -416,7 +416,7 @@ export class NotebookEditorWorkbenchToolbar extends Disposable { this._renderLabel = this._convertConfiguration(this.configurationService.getValue(NotebookSetting.globalToolbarShowLabel)); this._updateStrategy(); const oldElement = this._notebookLeftToolbar.getElement(); - oldElement.parentElement?.removeChild(oldElement); + oldElement.remove(); this._notebookLeftToolbar.dispose(); this._notebookLeftToolbar = this.instantiationService.createInstance( diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index ad475a844c5..7daf1889377 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -388,13 +388,13 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { description: suggestedExtension.displayName ?? suggestedExtension.extensionIds.join(', '), label: `$(${Codicon.lightbulb.id}) ` + localize('installSuggestedKernel', 'Install/Enable suggested extensions'), extensionIds: suggestedExtension.extensionIds - } as InstallExtensionPick); + } satisfies InstallExtensionPick); } // there is no kernel, show the install from marketplace quickPickItems.push({ id: 'install', label: localize('searchForKernels', "Browse marketplace for kernel extensions"), - } as SearchMarketplacePick); + } satisfies SearchMarketplacePick); return quickPickItems; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar.ts index 9856cbf900b..19b4df7f193 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar.ts @@ -113,9 +113,11 @@ export class ListTopCellToolbar extends Disposable { hiddenItemStrategy: HiddenItemStrategy.Ignore, }); - toolbar.context = { - notebookEditor: this.notebookEditor - }; + if (this.notebookEditor.hasModel()) { + toolbar.context = { + notebookEditor: this.notebookEditor + } satisfies INotebookActionContext; + } this.viewZone.value?.add(toolbar); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index b3b6ff6e2f9..21870a0067f 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -215,6 +215,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return this._alternativeVersionId; } + get notebookType() { + return this.viewType; + } + constructor( readonly viewType: string, readonly uri: URI, diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 33c85b27420..aea2596da48 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -8,36 +8,41 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDiffResult } from 'vs/base/common/diff/diff'; import { Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { ISplice } from 'vs/base/common/sequence'; +import { ThemeColor } from 'vs/base/common/themables'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { Range } from 'vs/editor/common/core/range'; import { ILineChange } from 'vs/editor/common/diff/legacyLinesDiffComputer'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { Command, WorkspaceEditMetadata } from 'vs/editor/common/languages'; import { IReadonlyTextBuffer } from 'vs/editor/common/model'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { ThemeColor } from 'vs/base/common/themables'; +import { IFileReadLimits } from 'vs/platform/files/common/files'; import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { ICellExecutionError } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { INotebookTextModelLike } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { generate as generateUri, parse as parseUri } from 'vs/workbench/services/notebook/common/notebookDocumentService'; import { IWorkingCopyBackupMeta, IWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { IFileReadLimits } from 'vs/platform/files/common/files'; -import { parse as parseUri, generate as generateUri } from 'vs/workbench/services/notebook/common/notebookDocumentService'; -import { ICellExecutionError } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; export const NOTEBOOK_EDITOR_ID = 'workbench.editor.notebook'; export const NOTEBOOK_DIFF_EDITOR_ID = 'workbench.editor.notebookTextDiffEditor'; export const INTERACTIVE_WINDOW_EDITOR_ID = 'workbench.editor.interactive'; +export const REPL_EDITOR_ID = 'workbench.editor.repl'; +export const EXECUTE_REPL_COMMAND_ID = 'replNotebook.input.execute'; export enum CellKind { Markup = 1, @@ -252,7 +257,8 @@ export interface ICell { onDidChangeInternalMetadata: Event; } -export interface INotebookTextModel { +export interface INotebookTextModel extends INotebookTextModelLike { + readonly notebookType: string; readonly viewType: string; metadata: NotebookDocumentMetadata; readonly transientOptions: TransientOptions; @@ -551,7 +557,7 @@ export interface INotebookContributionData { providerDisplayName: string; displayName: string; filenamePattern: (string | glob.IRelativePattern | INotebookExclusiveDocumentFilter)[]; - exclusive: boolean; + priority?: RegisteredEditorPriority; } @@ -776,6 +782,11 @@ export interface INotebookLoadOptions { readonly limits?: IFileReadLimits; } +export type NotebookEditorModelCreationOptions = { + limits?: IFileReadLimits; + scratchpad?: boolean; +}; + export interface IResolvedNotebookEditorModel extends INotebookEditorModel { notebook: NotebookTextModel; } @@ -818,7 +829,7 @@ export enum NotebookEditorPriority { option = 'option', } -export interface INotebookSearchOptions { +export interface INotebookFindOptions { regex?: boolean; wholeWord?: boolean; caseSensitive?: boolean; @@ -827,8 +838,19 @@ export interface INotebookSearchOptions { includeMarkupPreview?: boolean; includeCodeInput?: boolean; includeOutput?: boolean; - searchInRanges?: boolean; - selectedRanges?: ICellRange[]; + findScope?: INotebookFindScope; +} + +export interface INotebookFindScope { + findScopeType: NotebookFindScopeType; + selectedCellRanges?: ICellRange[]; + selectedTextRanges?: Range[]; +} + +export enum NotebookFindScopeType { + Cells = 'cells', + Text = 'text', + None = 'none' } export interface INotebookExclusiveDocumentFilter { @@ -954,7 +976,6 @@ export const NotebookSetting = { outputFontFamilyDeprecated: 'notebook.outputFontFamily', outputFontFamily: 'notebook.output.fontFamily', findFilters: 'notebook.find.filters', - findScope: 'notebook.experimental.find.scope.enabled', logging: 'notebook.logging', confirmDeleteRunningCell: 'notebook.confirmDeleteRunningCell', remoteSaving: 'notebook.experimental.remoteSave', diff --git a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index 4aad255f787..c4961dd6dad 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { INTERACTIVE_WINDOW_EDITOR_ID, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INTERACTIVE_WINDOW_EDITOR_ID, NOTEBOOK_EDITOR_ID, REPL_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -16,11 +16,15 @@ export const InteractiveWindowOpen = new RawContextKey('interactiveWind // Is Notebook export const NOTEBOOK_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', NOTEBOOK_EDITOR_ID); export const INTERACTIVE_WINDOW_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', INTERACTIVE_WINDOW_EDITOR_ID); +export const REPL_NOTEBOOK_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', REPL_EDITOR_ID); // Editor keys +// based on the focus of the notebook editor widget export const NOTEBOOK_EDITOR_FOCUSED = new RawContextKey('notebookEditorFocused', false); +// always true within the cell list html element export const NOTEBOOK_CELL_LIST_FOCUSED = new RawContextKey('notebookCellListFocused', false); export const NOTEBOOK_OUTPUT_FOCUSED = new RawContextKey('notebookOutputFocused', false); +// an input html element within the output webview has focus export const NOTEBOOK_OUTPUT_INPUT_FOCUSED = new RawContextKey('notebookOutputInputFocused', false); export const NOTEBOOK_EDITOR_EDITABLE = new RawContextKey('notebookEditable', true); export const NOTEBOOK_HAS_RUNNING_CELL = new RawContextKey('notebookHasRunningCell', false); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 0ac6de71100..5edcb35f62c 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -53,7 +53,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { static readonly ID: string = 'workbench.input.notebook'; - private _editorModelReference: IReference | null = null; + protected editorModelReference: IReference | null = null; private _sideLoadedListener: IDisposable; private _defaultDirtyState: boolean = false; @@ -105,8 +105,8 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { override dispose() { this._sideLoadedListener.dispose(); - this._editorModelReference?.dispose(); - this._editorModelReference = null; + this.editorModelReference?.dispose(); + this.editorModelReference = null; super.dispose(); } @@ -125,8 +125,8 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { capabilities |= EditorInputCapabilities.Untitled; } - if (this._editorModelReference) { - if (this._editorModelReference.object.isReadonly()) { + if (this.editorModelReference) { + if (this.editorModelReference.object.isReadonly()) { capabilities |= EditorInputCapabilities.Readonly; } } else { @@ -143,7 +143,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override getDescription(verbosity = Verbosity.MEDIUM): string | undefined { - if (!this.hasCapability(EditorInputCapabilities.Untitled) || this._editorModelReference?.object.hasAssociatedFilePath()) { + if (!this.hasCapability(EditorInputCapabilities.Untitled) || this.editorModelReference?.object.hasAssociatedFilePath()) { return super.getDescription(verbosity); } @@ -151,21 +151,21 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override isReadonly(): boolean | IMarkdownString { - if (!this._editorModelReference) { + if (!this.editorModelReference) { return this.filesConfigurationService.isReadonly(this.resource); } - return this._editorModelReference.object.isReadonly(); + return this.editorModelReference.object.isReadonly(); } override isDirty() { - if (!this._editorModelReference) { + if (!this.editorModelReference) { return this._defaultDirtyState; } - return this._editorModelReference.object.isDirty(); + return this.editorModelReference.object.isDirty(); } override isSaving(): boolean { - const model = this._editorModelReference?.object; + const model = this.editorModelReference?.object; if (!model || !model.isDirty() || model.hasErrorState || this.hasCapability(EditorInputCapabilities.Untitled)) { return false; // require the model to be dirty, file-backed and not in an error state } @@ -175,12 +175,12 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override async save(group: GroupIdentifier, options?: ISaveOptions): Promise { - if (this._editorModelReference) { + if (this.editorModelReference) { if (this.hasCapability(EditorInputCapabilities.Untitled)) { return this.saveAs(group, options); } else { - await this._editorModelReference.object.save(options); + await this.editorModelReference.object.save(options); } return this; @@ -190,7 +190,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { - if (!this._editorModelReference) { + if (!this.editorModelReference) { return undefined; } @@ -200,9 +200,9 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return undefined; } - const pathCandidate = this.hasCapability(EditorInputCapabilities.Untitled) ? await this._suggestName(provider, this.labelService.getUriBasenameLabel(this.resource)) : this._editorModelReference.object.resource; + const pathCandidate = this.hasCapability(EditorInputCapabilities.Untitled) ? await this._suggestName(provider, this.labelService.getUriBasenameLabel(this.resource)) : this.editorModelReference.object.resource; let target: URI | undefined; - if (this._editorModelReference.object.hasAssociatedFilePath()) { + if (this.editorModelReference.object.hasAssociatedFilePath()) { target = pathCandidate; } else { target = await this._fileDialogService.pickFileToSave(pathCandidate, options?.availableFileSystems); @@ -231,7 +231,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { throw new Error(`File name ${target} is not supported by ${provider.providerDisplayName}.\n\nPlease make sure the file name matches following patterns:\n${patterns}`); } - return await this._editorModelReference.object.saveAs(target); + return await this.editorModelReference.object.saveAs(target); } private async _suggestName(provider: NotebookProviderInfo, suggestedFilename: string) { @@ -260,7 +260,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { // called when users rename a notebook document override async rename(group: GroupIdentifier, target: URI): Promise { - if (this._editorModelReference) { + if (this.editorModelReference) { return { editor: { resource: target }, options: { override: this.viewType } }; } @@ -268,8 +268,8 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override async revert(_group: GroupIdentifier, options?: IRevertOptions): Promise { - if (this._editorModelReference && this._editorModelReference.object.isDirty()) { - await this._editorModelReference.object.revert(options); + if (this.editorModelReference && this.editorModelReference.object.isDirty()) { + await this.editorModelReference.object.revert(options); } } @@ -284,42 +284,43 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { // "other" loading anymore this._sideLoadedListener.dispose(); - if (!this._editorModelReference) { - const ref = await this._notebookModelResolverService.resolve(this.resource, this.viewType, this.ensureLimits(_options)); - if (this._editorModelReference) { + if (!this.editorModelReference) { + const scratchpad = this.capabilities & EditorInputCapabilities.Scratchpad ? true : false; + const ref = await this._notebookModelResolverService.resolve(this.resource, this.viewType, { limits: this.ensureLimits(_options), scratchpad }); + if (this.editorModelReference) { // Re-entrant, double resolve happened. Dispose the addition references and proceed // with the truth. ref.dispose(); - return (>this._editorModelReference).object; + return (>this.editorModelReference).object; } - this._editorModelReference = ref; + this.editorModelReference = ref; if (this.isDisposed()) { - this._editorModelReference.dispose(); - this._editorModelReference = null; + this.editorModelReference.dispose(); + this.editorModelReference = null; return null; } - this._register(this._editorModelReference.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); - this._register(this._editorModelReference.object.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); - this._register(this._editorModelReference.object.onDidRevertUntitled(() => this.dispose())); - if (this._editorModelReference.object.isDirty()) { + this._register(this.editorModelReference.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + this._register(this.editorModelReference.object.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); + this._register(this.editorModelReference.object.onDidRevertUntitled(() => this.dispose())); + if (this.editorModelReference.object.isDirty()) { this._onDidChangeDirty.fire(); } } else { - this._editorModelReference.object.load({ limits: this.ensureLimits(_options) }); + this.editorModelReference.object.load({ limits: this.ensureLimits(_options) }); } if (this.options._backupId) { - const info = await this._notebookService.withNotebookDataProvider(this._editorModelReference.object.notebook.viewType); + const info = await this._notebookService.withNotebookDataProvider(this.editorModelReference.object.notebook.viewType); if (!(info instanceof SimpleNotebookProviderInfo)) { throw new Error('CANNOT open file notebook with this provider'); } const data = await info.serializer.dataToNotebook(VSBuffer.fromString(JSON.stringify({ __webview_backup: this.options._backupId }))); - this._editorModelReference.object.notebook.applyEdits([ + this.editorModelReference.object.notebook.applyEdits([ { editType: CellEditType.Replace, index: 0, - count: this._editorModelReference.object.notebook.length, + count: this.editorModelReference.object.notebook.length, cells: data.cells } ], true, undefined, () => undefined, undefined, false); @@ -331,7 +332,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } } - return this._editorModelReference.object; + return this.editorModelReference.object; } override toUntyped(): IResourceEditorInput { diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 3c26eff229a..a6a06d63186 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -15,6 +15,7 @@ import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWriteFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; @@ -197,7 +198,8 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF private readonly _notebookModel: NotebookTextModel, private readonly _notebookService: INotebookService, private readonly _configurationService: IConfigurationService, - private readonly _telemetryService: ITelemetryService + private readonly _telemetryService: ITelemetryService, + private readonly _logService: ILogService ) { super(); @@ -237,13 +239,22 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF } private async setSaveDelegate() { - const serializer = await this.getNotebookSerializer(); - this.save = async (options: IWriteFileOptions, token: CancellationToken) => { - if (token.isCancellationRequested) { - throw new CancellationError(); - } + // make sure we wait for a serializer to resolve before we try to handle saves in the EH + await this.getNotebookSerializer(); + this.save = async (options: IWriteFileOptions, token: CancellationToken) => { try { + let serializer = this._notebookService.tryGetDataProviderSync(this.notebookModel.viewType)?.serializer; + + if (!serializer) { + this._logService.warn('No serializer found for notebook model, checking if provider still needs to be resolved'); + serializer = await this.getNotebookSerializer(); + } + + if (token.isCancellationRequested) { + throw new CancellationError(); + } + const stat = await serializer.save(this._notebookModel.uri, this._notebookModel.versionId, options, token); return stat; } catch (error) { @@ -358,7 +369,8 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo private readonly _viewType: string, @INotebookService private readonly _notebookService: INotebookService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITelemetryService private readonly _telemetryService: ITelemetryService + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ILogService private readonly _logService: ILogService ) { } async createModel(resource: URI, stream: VSBufferReadableStream, token: CancellationToken): Promise { @@ -376,7 +388,7 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo } const notebookModel = this._notebookService.createNotebookTextModel(info.viewType, resource, data, info.serializer.options); - return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService, this._configurationService, this._telemetryService); + return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService, this._configurationService, this._telemetryService, this._logService); } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts index 3eff1eb5060..dc3d26a0ca3 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts @@ -5,10 +5,9 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IResolvedNotebookEditorModel, NotebookEditorModelCreationOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IReference } from 'vs/base/common/lifecycle'; import { Event, IWaitUntil } from 'vs/base/common/event'; -import { IFileReadLimits } from 'vs/platform/files/common/files'; export const INotebookEditorModelResolverService = createDecorator('INotebookModelResolverService'); @@ -50,6 +49,6 @@ export interface INotebookEditorModelResolverService { isDirty(resource: URI): boolean; - resolve(resource: URI, viewType?: string, limits?: IFileReadLimits): Promise>; - resolve(resource: IUntitledNotebookResource, viewType: string, limits?: IFileReadLimits): Promise>; + resolve(resource: URI, viewType?: string, creationOptions?: NotebookEditorModelCreationOptions): Promise>; + resolve(resource: IUntitledNotebookResource, viewType: string, creationOtions?: NotebookEditorModelCreationOptions): Promise>; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index 9d0a90e4576..d43273d4fa6 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -5,7 +5,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { CellUri, IResolvedNotebookEditorModel, NotebookSetting, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, IResolvedNotebookEditorModel, NotebookEditorModelCreationOptions, NotebookSetting, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModelFactory, SimpleNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, ReferenceCollection, toDisposable } from 'vs/base/common/lifecycle'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -61,7 +61,7 @@ class NotebookModelReferenceCollection extends ReferenceCollection { + protected async createReferencedObject(key: string, viewType: string, hasAssociatedFilePath: boolean, limits?: IFileReadLimits, isScratchpad?: boolean): Promise { // Untrack as being disposed this.modelsToDispose.delete(key); @@ -70,7 +70,7 @@ class NotebookModelReferenceCollection extends ReferenceCollection>this._instantiationService.createInstance( FileWorkingCopyManager, workingCopyTypeId, @@ -79,8 +79,9 @@ class NotebookModelReferenceCollection extends ReferenceCollection(NotebookSetting.InteractiveWindowPromptToSave) !== true; - const model = this._instantiationService.createInstance(SimpleNotebookEditorModel, uri, hasAssociatedFilePath, viewType, workingCopyManager, scratchPad); + + const isScratchpadView = isScratchpad || (viewType === 'interactive' && this._configurationService.getValue(NotebookSetting.InteractiveWindowPromptToSave) !== true); + const model = this._instantiationService.createInstance(SimpleNotebookEditorModel, uri, hasAssociatedFilePath, viewType, workingCopyManager, isScratchpadView); const result = await model.load({ limits }); @@ -176,9 +177,9 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes return this._data.isDirty(resource); } - async resolve(resource: URI, viewType?: string, limits?: IFileReadLimits): Promise>; - async resolve(resource: IUntitledNotebookResource, viewType: string, limits?: IFileReadLimits): Promise>; - async resolve(arg0: URI | IUntitledNotebookResource, viewType?: string, limits?: IFileReadLimits): Promise> { + async resolve(resource: URI, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise>; + async resolve(resource: IUntitledNotebookResource, viewType: string, options: NotebookEditorModelCreationOptions): Promise>; + async resolve(arg0: URI | IUntitledNotebookResource, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise> { let resource: URI; let hasAssociatedFilePath = false; if (URI.isUri(arg0)) { @@ -219,8 +220,9 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes } else { await this._extensionService.whenInstalledExtensionsRegistered(); const providers = this._notebookService.getContributedNotebookTypes(resource); - const exclusiveProvider = providers.find(provider => provider.exclusive); - viewType = exclusiveProvider?.id || providers[0]?.id; + viewType = providers.find(provider => provider.priority === 'exclusive')?.id ?? + providers.find(provider => provider.priority === 'default')?.id ?? + providers[0]?.id; } } @@ -239,7 +241,7 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes } } - const reference = this._data.acquire(resource.toString(), viewType, hasAssociatedFilePath, limits); + const reference = this._data.acquire(resource.toString(), viewType, hasAssociatedFilePath, options?.limits, options?.scratchpad); try { const model = await reference.object; return { diff --git a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts index be999203d75..2351e4b42b2 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts @@ -106,7 +106,7 @@ export interface IKernelSourceActionProvider { provideKernelSourceActions(): Promise; } -export interface INotebookTextModelLike { uri: URI; viewType: string } +export interface INotebookTextModelLike { uri: URI; notebookType: string } export const INotebookKernelService = createDecorator('INotebookKernelService'); diff --git a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts index 16a9ee3a57b..e2911dcf9e1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts @@ -19,7 +19,6 @@ export interface NotebookEditorDescriptor { readonly selectors: readonly { filenamePattern?: string; excludeFileNamePattern?: string }[]; readonly priority: RegisteredEditorPriority; readonly providerDisplayName: string; - readonly exclusive: boolean; } export class NotebookProviderInfo { @@ -29,7 +28,6 @@ export class NotebookProviderInfo { readonly displayName: string; readonly priority: RegisteredEditorPriority; readonly providerDisplayName: string; - readonly exclusive: boolean; private _selectors: NotebookSelector[]; get selectors() { @@ -50,7 +48,6 @@ export class NotebookProviderInfo { })) || []; this.priority = descriptor.priority; this.providerDisplayName = descriptor.providerDisplayName; - this.exclusive = descriptor.exclusive; this._options = { transientCellMetadata: {}, transientDocumentMetadata: {}, diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 74614b41441..8a000471ecf 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -68,6 +68,7 @@ export interface INotebookService { registerNotebookSerializer(viewType: string, extensionData: NotebookExtensionDescription, serializer: INotebookSerializer): IDisposable; withNotebookDataProvider(viewType: string): Promise; + tryGetDataProviderSync(viewType: string): SimpleNotebookProviderInfo | undefined; getOutputMimeTypeInfo(textModel: NotebookTextModel, kernelProvides: readonly string[] | undefined, output: IOutputDto): readonly IOrderedMimeType[]; diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellDecorations.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellDecorations.test.ts index 17fbe22abaa..7063f688505 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/cellDecorations.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/cellDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts index 0db4fb19201..5867385c0aa 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts @@ -6,7 +6,7 @@ import { performCellDropEdits } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; -import * as assert from 'assert'; +import assert from 'assert'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts index 357927dbf60..65ea08b7f27 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; import { changeCellToKind, computeCellLinesContents, copyCellRange, insertCell, joinNotebookCells, moveCellRange, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CellEditType, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts index fefb3d36cff..13b2d31e380 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/executionStatusBarItem.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/executionStatusBarItem.test.ts index 80931b652e6..342bbd90a93 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/executionStatusBarItem.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/executionStatusBarItem.test.ts @@ -8,7 +8,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { formatCellDuration } from 'vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts index c8f3a958085..7f9571b91e1 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; import { FindMatch, ITextBuffer, ValidAnnotatedEditOperation } from 'vs/editor/common/model'; import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/core/wordHelper'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/layoutActions.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/layoutActions.test.ts index 886806afb68..11a24c15fec 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/layoutActions.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/layoutActions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ToggleCellToolbarPositionAction } from 'vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts index ef1c965adb2..86636c92a0b 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookClipboard.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookClipboard.test.ts index de3e3c30e92..501dcb337c9 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookClipboard.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookClipboard.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { NotebookClipboardContribution, runCopyCells, runCutCells } from 'vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard'; import { CellKind, NOTEBOOK_EDITOR_ID, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts index 9145f0bf00b..ed6e5f6363b 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; import { IFileIconTheme, IThemeService } from 'vs/platform/theme/common/themeService'; @@ -18,6 +18,9 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { NotebookCellOutline } from 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; +import { IEditorPaneSelectionChangeEvent } from 'vs/workbench/common/editor'; suite('Notebook Outline', function () { @@ -32,6 +35,7 @@ suite('Notebook Outline', function () { disposables = new DisposableStore(); instantiationService = setupInstantiationService(disposables); instantiationService.set(IEditorService, new class extends mock() { }); + instantiationService.set(ILanguageFeaturesService, new LanguageFeaturesService()); instantiationService.set(IMarkerService, disposables.add(new MarkerService())); instantiationService.set(IThemeService, new class extends mock() { override onDidFileIconThemeChange = Event.None; @@ -52,6 +56,7 @@ suite('Notebook Outline', function () { return editor; } override onDidChangeModel: Event = Event.None; + override onDidChangeSelection: Event = Event.None; }, OutlineTarget.OutlinePane); disposables.add(outline); diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts index e9d26500bed..8f4eb76f909 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDataSource } from 'vs/base/browser/ui/tree/tree'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IReference } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ITextModel } from 'vs/editor/common/model'; @@ -14,15 +15,17 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { NotebookBreadcrumbsProvider, NotebookCellOutline, NotebookOutlinePaneProvider, NotebookQuickPickProvider } from 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookCellOutlineDataSource } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource'; import { NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory'; import { OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/OutlineEntry'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { MockDocumentSymbol } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; -import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; suite('Notebook Outline View Providers', function () { + // #region Setup - ensureNoDisposablesAreLeakedInTestSuite(); + + const store = ensureNoDisposablesAreLeakedInTestSuite(); const configurationService = new TestConfigurationService(); const themeService = new TestThemeService(); @@ -52,8 +55,10 @@ suite('Notebook Outline View Providers', function () { return 0; } }; + // #endregion // #region Helpers + function createCodeCellViewModel(version: number = 1, source = '# code', textmodelId = 'textId') { return { textBuffer: { @@ -75,6 +80,15 @@ suite('Notebook Outline View Providers', function () { } as ICellViewModel; } + function createMockOutlineDataSource(entries: OutlineEntry[], activeElement: OutlineEntry | undefined = undefined) { + return new class extends mock>() { + override object: INotebookCellOutlineDataSource = { + entries: entries, + activeElement: activeElement, + }; + }; + } + function createMarkupCellViewModel(version: number = 1, source = 'markup', textmodelId = 'textId', alternativeId = 1) { return { textBuffer: { @@ -99,7 +113,7 @@ suite('Notebook Outline View Providers', function () { } as ICellViewModel; } - function flatten(element: NotebookCellOutline | OutlineEntry, dataSource: IDataSource): OutlineEntry[] { + function flatten(element: OutlineEntry, dataSource: IDataSource): OutlineEntry[] { const elements: OutlineEntry[] = []; const children = dataSource.getChildren(element); @@ -166,6 +180,7 @@ suite('Notebook Outline View Providers', function () { await configurationService.setUserConfiguration('notebook.gotoSymbols.showAllSymbols', config.quickPickShowAllSymbols); await configurationService.setUserConfiguration('notebook.breadcrumbs.showCodeCells', config.breadcrumbsShowCodeCells); } + // #endregion // #region OutlinePane @@ -199,11 +214,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const outlinePaneProvider = new NotebookOutlinePaneProvider(() => [], configurationService); + const outlinePaneProvider = store.add(new NotebookOutlinePaneProvider(undefined, configurationService)); const results = flatten(outlineModel, outlinePaneProvider); // Validate @@ -242,11 +257,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const outlinePaneProvider = new NotebookOutlinePaneProvider(() => [], configurationService); + const outlinePaneProvider = store.add(new NotebookOutlinePaneProvider(undefined, configurationService)); const results = flatten(outlineModel, outlinePaneProvider); assert.equal(results.length, 2); @@ -288,11 +303,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const outlinePaneProvider = new NotebookOutlinePaneProvider(() => [], configurationService); + const outlinePaneProvider = store.add(new NotebookOutlinePaneProvider(undefined, configurationService)); const results = flatten(outlineModel, outlinePaneProvider); assert.equal(results.length, 1); @@ -331,11 +346,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const outlinePaneProvider = new NotebookOutlinePaneProvider(() => [], configurationService); + const outlinePaneProvider = store.add(new NotebookOutlinePaneProvider(undefined, configurationService)); const results = flatten(outlineModel, outlinePaneProvider); assert.equal(results.length, 3); @@ -380,11 +395,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const outlinePaneProvider = new NotebookOutlinePaneProvider(() => [], configurationService); + const outlinePaneProvider = store.add(new NotebookOutlinePaneProvider(undefined, configurationService)); const results = flatten(outlineModel, outlinePaneProvider); // validate @@ -439,11 +454,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const quickPickProvider = new NotebookQuickPickProvider(() => [...outlineModel.children], configurationService, themeService); + const quickPickProvider = store.add(new NotebookQuickPickProvider(createMockOutlineDataSource([...outlineModel.children]), configurationService, themeService)); const results = quickPickProvider.getQuickPickElements(); // Validate @@ -492,11 +507,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const quickPickProvider = new NotebookQuickPickProvider(() => [...outlineModel.children], configurationService, themeService); + const quickPickProvider = store.add(new NotebookQuickPickProvider(createMockOutlineDataSource([...outlineModel.children]), configurationService, themeService)); const results = quickPickProvider.getQuickPickElements(); // Validate @@ -545,11 +560,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const quickPickProvider = new NotebookQuickPickProvider(() => [...outlineModel.children], configurationService, themeService); + const quickPickProvider = store.add(new NotebookQuickPickProvider(createMockOutlineDataSource([...outlineModel.children]), configurationService, themeService)); const results = quickPickProvider.getQuickPickElements(); // Validate @@ -601,12 +616,12 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createMarkupCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } const outlineTree = buildOutlineTree([...outlineModel.children]); // Generate filtered outline (view model) - const breadcrumbsProvider = new NotebookBreadcrumbsProvider(() => [...outlineTree![0].children][1], configurationService); + const breadcrumbsProvider = store.add(new NotebookBreadcrumbsProvider(createMockOutlineDataSource([], [...outlineTree![0].children][1]), configurationService)); const results = breadcrumbsProvider.getBreadcrumbElements(); // Validate @@ -652,12 +667,12 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createMarkupCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } const outlineTree = buildOutlineTree([...outlineModel.children]); // Generate filtered outline (view model) - const breadcrumbsProvider = new NotebookBreadcrumbsProvider(() => [...outlineTree![0].children][1], configurationService); + const breadcrumbsProvider = store.add(new NotebookBreadcrumbsProvider(createMockOutlineDataSource([], [...outlineTree![0].children][1]), configurationService)); const results = breadcrumbsProvider.getBreadcrumbElements(); // Validate diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts index 90d732a0cf1..dfe5b81e3f4 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -13,7 +13,6 @@ import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBr import { NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { MockDocumentSymbol } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; -import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; suite('Notebook Symbols', function () { ensureNoDisposablesAreLeakedInTestSuite(); @@ -67,7 +66,7 @@ suite('Notebook Symbols', function () { test('Cell without symbols cache', function () { setSymbolsForTextModel([{ name: 'var', range: {} }]); const entryFactory = new NotebookOutlineEntryFactory(executionService); - const entries = entryFactory.getOutlineEntries(createCellViewModel(), OutlineTarget.QuickPick, 0); + const entries = entryFactory.getOutlineEntries(createCellViewModel(), 0); assert.equal(entries.length, 1, 'no entries created'); assert.equal(entries[0].label, '# code', 'entry should fall back to first line of cell'); @@ -79,7 +78,7 @@ suite('Notebook Symbols', function () { const cell = createCellViewModel(); await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); - const entries = entryFactory.getOutlineEntries(cell, OutlineTarget.QuickPick, 0); + const entries = entryFactory.getOutlineEntries(cell, 0); assert.equal(entries.length, 3, 'wrong number of outline entries'); assert.equal(entries[0].label, '# code'); @@ -101,7 +100,7 @@ suite('Notebook Symbols', function () { const cell = createCellViewModel(); await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); - const entries = entryFactory.getOutlineEntries(createCellViewModel(), OutlineTarget.QuickPick, 0); + const entries = entryFactory.getOutlineEntries(createCellViewModel(), 0); assert.equal(entries.length, 6, 'wrong number of outline entries'); assert.equal(entries[0].label, '# code'); @@ -127,8 +126,8 @@ suite('Notebook Symbols', function () { await entryFactory.cacheSymbols(cell1, outlineModelService, CancellationToken.None); await entryFactory.cacheSymbols(cell2, outlineModelService, CancellationToken.None); - const entries1 = entryFactory.getOutlineEntries(createCellViewModel(1, '$1'), OutlineTarget.QuickPick, 0); - const entries2 = entryFactory.getOutlineEntries(createCellViewModel(1, '$2'), OutlineTarget.QuickPick, 0); + const entries1 = entryFactory.getOutlineEntries(createCellViewModel(1, '$1'), 0); + const entries2 = entryFactory.getOutlineEntries(createCellViewModel(1, '$2'), 0); assert.equal(entries1.length, 2, 'wrong number of outline entries'); diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookUndoRedo.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookUndoRedo.test.ts index 98628d0c1b9..0770113f20a 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookUndoRedo.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookUndoRedo.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CellEditType, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createNotebookCellList, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/outputCopyTests.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/outputCopyTests.test.ts index 066d89cc736..ade4aa5abc3 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/outputCopyTests.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/outputCopyTests.test.ts @@ -7,7 +7,7 @@ import { ICellOutputViewModel, ICellViewModel } from 'vs/workbench/contrib/noteb import { mock } from 'vs/base/test/common/mock'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ILogService } from 'vs/platform/log/common/log'; -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { IOutputItemDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { copyCellOutput } from 'vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookBrowser.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookBrowser.test.ts index 9b6eb24d208..dedeec1068b 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookBrowser.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookBrowser.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookCellAnchor.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookCellAnchor.test.ts index 782c8145df2..b2dfc2be7ff 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookCellAnchor.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookCellAnchor.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts index 6f9de776b53..f62c916928e 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts index d18126e162b..d0c5bc4bd91 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts index 654fe7ee807..f2bb130dba9 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { ISequence, LcsDiff } from 'vs/base/common/diff/diff'; import { Mimes } from 'vs/base/common/mime'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts index 1bd5ca8f03c..8bfdf049701 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts index 071017d82d6..a7849fd7319 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -15,6 +15,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellKind, IOutputDto, NotebookData, NotebookSetting, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -28,7 +29,10 @@ suite('NotebookFileWorkingCopyModel', function () { let disposables: DisposableStore; let instantiationService: TestInstantiationService; const configurationService = new TestConfigurationService(); - const telemetryService = new class extends mock() { }; + const telemetryService = new class extends mock() { + override publicLogError2() { } + }; + const logservice = new class extends mock() { }; teardown(() => disposables.dispose()); @@ -65,7 +69,8 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService, - telemetryService + telemetryService, + logservice )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -88,7 +93,8 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService, - telemetryService + telemetryService, + logservice )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); assert.strictEqual(callCount, 1); @@ -123,7 +129,8 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService, - telemetryService + telemetryService, + logservice )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -147,6 +154,7 @@ suite('NotebookFileWorkingCopyModel', function () { ), configurationService, telemetryService, + logservice )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -181,7 +189,8 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService, - telemetryService + telemetryService, + logservice )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -204,7 +213,8 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService, - telemetryService + telemetryService, + logservice )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); assert.strictEqual(callCount, 1); @@ -239,7 +249,8 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService, - telemetryService + telemetryService, + logservice )); try { @@ -282,7 +293,8 @@ suite('NotebookFileWorkingCopyModel', function () { notebook, notebookService, configurationService, - telemetryService + telemetryService, + logservice )); // the save method should not be set if the serializer is not yet resolved @@ -299,11 +311,25 @@ suite('NotebookFileWorkingCopyModel', function () { function mockNotebookService(notebook: NotebookTextModel, notebookSerializer: Promise | INotebookSerializer) { return new class extends mock() { + private serializer: INotebookSerializer | undefined = undefined; override async withNotebookDataProvider(viewType: string): Promise { - const serializer = await notebookSerializer; + this.serializer = await notebookSerializer; + return new SimpleNotebookProviderInfo( + notebook.viewType, + this.serializer, + { + id: new ExtensionIdentifier('test'), + location: undefined + } + ); + } + override tryGetDataProviderSync(viewType: string): SimpleNotebookProviderInfo | undefined { + if (!this.serializer) { + return undefined; + } return new SimpleNotebookProviderInfo( notebook.viewType, - serializer, + this.serializer, { id: new ExtensionIdentifier('test'), location: undefined diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts index c4b00528450..09731c28e5d 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { AsyncIterableObject } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -11,7 +11,7 @@ import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; -import { assertThrowsAsync } from 'vs/base/test/common/utils'; +import { assertThrowsAsync, ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { IMenu, IMenuService } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -40,6 +40,8 @@ suite('NotebookExecutionService', () => { disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + setup(function () { disposables = new DisposableStore(); @@ -80,8 +82,8 @@ suite('NotebookExecutionService', () => { contextKeyService = instantiationService.get(IContextKeyService); }); - async function withTestNotebook(cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (viewModel: NotebookViewModel, textModel: NotebookTextModel) => void | Promise) { - return _withTestNotebook(cells, (editor, viewModel) => callback(viewModel, viewModel.notebookDocument)); + async function withTestNotebook(cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (viewModel: NotebookViewModel, textModel: NotebookTextModel, disposables: DisposableStore) => void | Promise) { + return _withTestNotebook(cells, (editor, viewModel, disposables) => callback(viewModel, viewModel.notebookDocument, disposables)); } // test('ctor', () => { @@ -94,7 +96,7 @@ suite('NotebookExecutionService', () => { test('cell is not runnable when no kernel is selected', async () => { await withTestNotebook( [], - async (viewModel, textModel) => { + async (viewModel, textModel, disposables) => { const executionService = instantiationService.createInstance(NotebookExecutionService); const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); @@ -107,9 +109,9 @@ suite('NotebookExecutionService', () => { [], async (viewModel, textModel) => { - kernelService.registerKernel(new TestNotebookKernel({ languages: ['testlang'] })); - const executionService = instantiationService.createInstance(NotebookExecutionService); - const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + disposables.add(kernelService.registerKernel(new TestNotebookKernel({ languages: ['testlang'] }))); + const executionService = disposables.add(instantiationService.createInstance(NotebookExecutionService)); + const cell = disposables.add(insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); await assertThrowsAsync(async () => await executionService.executeNotebookCells(textModel, [cell.model], contextKeyService)); }); @@ -120,13 +122,13 @@ suite('NotebookExecutionService', () => { [], async (viewModel, textModel) => { const kernel = new TestNotebookKernel({ languages: ['javascript'] }); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, textModel); - const executionService = instantiationService.createInstance(NotebookExecutionService); + const executionService = disposables.add(instantiationService.createInstance(NotebookExecutionService)); const executeSpy = sinon.spy(); kernel.executeNotebookCellsRequest = executeSpy; - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); await executionService.executeNotebookCells(viewModel.notebookDocument, [cell.model], contextKeyService); assert.strictEqual(executeSpy.calledOnce, true); }); @@ -148,12 +150,12 @@ suite('NotebookExecutionService', () => { } }; - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, textModel); - const executionService = instantiationService.createInstance(NotebookExecutionService); + const executionService = disposables.add(instantiationService.createInstance(NotebookExecutionService)); const exeStateService = instantiationService.get(INotebookExecutionStateService); - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); await executionService.executeNotebookCells(textModel, [cell.model], contextKeyService); assert.strictEqual(didExecute, true); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts index 7087aa9ec1a..8dd95c484df 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { AsyncIterableObject, DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { IMenu, IMenuService } from 'vs/platform/actions/common/actions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -38,6 +39,8 @@ suite('NotebookExecutionStateService', () => { disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + setup(function () { disposables = new DisposableStore(); @@ -69,12 +72,12 @@ suite('NotebookExecutionStateService', () => { instantiationService.set(INotebookExecutionStateService, disposables.add(instantiationService.createInstance(NotebookExecutionStateService))); }); - async function withTestNotebook(cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (viewModel: NotebookViewModel, textModel: NotebookTextModel) => void | Promise) { - return _withTestNotebook(cells, (editor, viewModel) => callback(viewModel, viewModel.notebookDocument)); + async function withTestNotebook(cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (viewModel: NotebookViewModel, textModel: NotebookTextModel, disposables: DisposableStore) => void | Promise) { + return _withTestNotebook(cells, (editor, viewModel) => callback(viewModel, viewModel.notebookDocument, disposables)); } function testCancelOnDelete(expectedCancels: number, implementsInterrupt: boolean) { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; let cancels = 0; @@ -91,15 +94,15 @@ suite('NotebookExecutionStateService', () => { cancels += handles.length; } }; - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); // Should cancel executing and pending cells, when kernel does not implement interrupt - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - const cell2 = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - const cell3 = insertCellAtIndex(viewModel, 2, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); + const cell2 = disposables.add(insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); + const cell3 = disposables.add(insertCellAtIndex(viewModel, 2, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); insertCellAtIndex(viewModel, 3, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); // Not deleted const exe = executionStateService.createCellExecution(viewModel.uri, cell.handle); // Executing exe.confirm(); @@ -126,11 +129,11 @@ suite('NotebookExecutionStateService', () => { }); test('fires onDidChangeCellExecution when cell is completed while deleted', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); @@ -153,15 +156,15 @@ suite('NotebookExecutionStateService', () => { }); test('does not fire onDidChangeCellExecution for output updates', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); const exe = executionStateService.createCellExecution(viewModel.uri, cell.handle); let didFire = false; @@ -181,15 +184,15 @@ suite('NotebookExecutionStateService', () => { // #142466 test('getCellExecution and onDidChangeCellExecution', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); const deferred = new DeferredPromise(); disposables.add(executionStateService.onDidChangeExecution(e => { @@ -213,11 +216,11 @@ suite('NotebookExecutionStateService', () => { }); }); test('getExecution and onDidChangeExecution', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const eventRaisedWithExecution: boolean[] = []; @@ -243,11 +246,11 @@ suite('NotebookExecutionStateService', () => { }); test('getExecution and onDidChangeExecution 2', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); @@ -283,15 +286,15 @@ suite('NotebookExecutionStateService', () => { }); test('force-cancel works for Cell Execution', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); executionStateService.createCellExecution(viewModel.uri, cell.handle); const exe = executionStateService.getCellExecution(cell.uri); assert.ok(exe); @@ -302,11 +305,11 @@ suite('NotebookExecutionStateService', () => { }); }); test('force-cancel works for Notebook Execution', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const eventRaisedWithExecution: boolean[] = []; @@ -324,11 +327,11 @@ suite('NotebookExecutionStateService', () => { }); }); test('force-cancel works for Cell and Notebook Execution', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookFolding.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookFolding.test.ts index 4acfd482bf8..0d397b63bf6 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookFolding.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookFolding.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts index a476f5caead..a037768bbed 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +import { setupInstantiationService } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { Emitter, Event } from 'vs/base/common/event'; import { INotebookKernel, INotebookKernelService, VariablesResult } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl'; @@ -66,8 +66,8 @@ suite('NotebookKernelHistoryService', () => { const u1 = URI.parse('foo:///one'); - const k1 = new TestNotebookKernel({ label: 'z', viewType: 'foo' }); - const k2 = new TestNotebookKernel({ label: 'a', viewType: 'foo' }); + const k1 = new TestNotebookKernel({ label: 'z', notebookType: 'foo' }); + const k2 = new TestNotebookKernel({ label: 'a', notebookType: 'foo' }); disposables.add(kernelService.registerKernel(k1)); disposables.add(kernelService.registerKernel(k2)); @@ -102,14 +102,14 @@ suite('NotebookKernelHistoryService', () => { const kernelHistoryService = disposables.add(instantiationService.createInstance(NotebookKernelHistoryService)); - let info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + let info = kernelHistoryService.getKernels({ uri: u1, notebookType: 'foo' }); assert.equal(info.all.length, 0); assert.ok(!info.selected); // update priorities for u1 notebook kernelService.updateKernelNotebookAffinity(k2, u1, 2); - info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + info = kernelHistoryService.getKernels({ uri: u1, notebookType: 'foo' }); assert.equal(info.all.length, 0); // MRU only auto selects kernel if there is only one assert.deepStrictEqual(info.selected, undefined); @@ -119,9 +119,9 @@ suite('NotebookKernelHistoryService', () => { const u1 = URI.parse('foo:///one'); - const k1 = new TestNotebookKernel({ label: 'z', viewType: 'foo' }); - const k2 = new TestNotebookKernel({ label: 'a', viewType: 'foo' }); - const k3 = new TestNotebookKernel({ label: 'b', viewType: 'foo' }); + const k1 = new TestNotebookKernel({ label: 'z', notebookType: 'foo' }); + const k2 = new TestNotebookKernel({ label: 'a', notebookType: 'foo' }); + const k3 = new TestNotebookKernel({ label: 'b', notebookType: 'foo' }); disposables.add(kernelService.registerKernel(k1)); disposables.add(kernelService.registerKernel(k2)); @@ -158,12 +158,12 @@ suite('NotebookKernelHistoryService', () => { }); const kernelHistoryService = disposables.add(instantiationService.createInstance(NotebookKernelHistoryService)); - let info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + let info = kernelHistoryService.getKernels({ uri: u1, notebookType: 'foo' }); assert.equal(info.all.length, 1); assert.deepStrictEqual(info.selected, undefined); kernelHistoryService.addMostRecentKernel(k3); - info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + info = kernelHistoryService.getKernels({ uri: u1, notebookType: 'foo' }); assert.deepStrictEqual(info.all, [k3, k2]); }); }); @@ -190,9 +190,9 @@ class TestNotebookKernel implements INotebookKernel { return AsyncIterableObject.EMPTY; } - constructor(opts?: { languages?: string[]; label?: string; viewType?: string }) { + constructor(opts?: { languages?: string[]; label?: string; notebookType?: string }) { this.supportedLanguages = opts?.languages ?? [PLAINTEXT_LANGUAGE_ID]; this.label = opts?.label ?? this.label; - this.viewType = opts?.viewType ?? this.viewType; + this.viewType = opts?.notebookType ?? this.viewType; } } diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts index 1ed657a1972..b05cd81e6a7 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +import { setupInstantiationService } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { Emitter, Event } from 'vs/base/common/event'; import { INotebookKernel, INotebookKernelService, VariablesResult } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl'; @@ -72,7 +72,7 @@ suite('NotebookKernelService', () => { disposables.add(kernelService.registerKernel(k2)); // equal priorities -> sort by name - let info = kernelService.getMatchingKernel({ uri: u1, viewType: 'foo' }); + let info = kernelService.getMatchingKernel({ uri: u1, notebookType: 'foo' }); assert.ok(info.all[0] === k2); assert.ok(info.all[1] === k1); @@ -81,18 +81,18 @@ suite('NotebookKernelService', () => { kernelService.updateKernelNotebookAffinity(k2, u2, 1); // updated - info = kernelService.getMatchingKernel({ uri: u1, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: u1, notebookType: 'foo' }); assert.ok(info.all[0] === k2); assert.ok(info.all[1] === k1); // NOT updated - info = kernelService.getMatchingKernel({ uri: u2, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: u2, notebookType: 'foo' }); assert.ok(info.all[0] === k2); assert.ok(info.all[1] === k1); // reset kernelService.updateKernelNotebookAffinity(k2, u1, undefined); - info = kernelService.getMatchingKernel({ uri: u1, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: u1, notebookType: 'foo' }); assert.ok(info.all[0] === k2); assert.ok(info.all[1] === k1); }); @@ -103,18 +103,18 @@ suite('NotebookKernelService', () => { const kernel = new TestNotebookKernel(); disposables.add(kernelService.registerKernel(kernel)); - let info = kernelService.getMatchingKernel({ uri: notebook, viewType: 'foo' }); + let info = kernelService.getMatchingKernel({ uri: notebook, notebookType: 'foo' }); assert.strictEqual(info.all.length, 1); assert.ok(info.all[0] === kernel); const betterKernel = new TestNotebookKernel(); disposables.add(kernelService.registerKernel(betterKernel)); - info = kernelService.getMatchingKernel({ uri: notebook, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: notebook, notebookType: 'foo' }); assert.strictEqual(info.all.length, 2); kernelService.updateKernelNotebookAffinity(betterKernel, notebook, 2); - info = kernelService.getMatchingKernel({ uri: notebook, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: notebook, notebookType: 'foo' }); assert.strictEqual(info.all.length, 2); assert.ok(info.all[0] === betterKernel); assert.ok(info.all[1] === kernel); @@ -123,8 +123,8 @@ suite('NotebookKernelService', () => { test('onDidChangeSelectedNotebooks not fired on initial notebook open #121904', function () { const uri = URI.parse('foo:///one'); - const jupyter = { uri, viewType: 'jupyter' }; - const dotnet = { uri, viewType: 'dotnet' }; + const jupyter = { uri, viewType: 'jupyter', notebookType: 'jupyter' }; + const dotnet = { uri, viewType: 'dotnet', notebookType: 'dotnet' }; const jupyterKernel = new TestNotebookKernel({ viewType: jupyter.viewType }); const dotnetKernel = new TestNotebookKernel({ viewType: dotnet.viewType }); @@ -144,8 +144,8 @@ suite('NotebookKernelService', () => { test('onDidChangeSelectedNotebooks not fired on initial notebook open #121904, p2', async function () { const uri = URI.parse('foo:///one'); - const jupyter = { uri, viewType: 'jupyter' }; - const dotnet = { uri, viewType: 'dotnet' }; + const jupyter = { uri, viewType: 'jupyter', notebookType: 'jupyter' }; + const dotnet = { uri, viewType: 'dotnet', notebookType: 'dotnet' }; const jupyterKernel = new TestNotebookKernel({ viewType: jupyter.viewType }); const dotnetKernel = new TestNotebookKernel({ viewType: dotnet.viewType }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookRendererMessagingService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookRendererMessagingService.test.ts index db9868edfa7..d40658ab353 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookRendererMessagingService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookRendererMessagingService.test.ts @@ -6,7 +6,7 @@ import { NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { stub } from 'sinon'; import { NotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/browser/services/notebookRendererMessagingServiceImpl'; -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookSelection.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookSelection.test.ts index 5bbbf50a502..0fd1c9a92fe 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookSelection.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookSelection.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts index 647c96305ce..77ce4841a79 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -56,7 +56,6 @@ suite('NotebookProviderInfoStore', function () { displayName: 'foo', selectors: [{ filenamePattern: '*.foo' }], priority: RegisteredEditorPriority.default, - exclusive: false, providerDisplayName: 'foo', }); const barInfo = new NotebookProviderInfo({ @@ -65,7 +64,6 @@ suite('NotebookProviderInfoStore', function () { displayName: 'bar', selectors: [{ filenamePattern: '*.bar' }], priority: RegisteredEditorPriority.default, - exclusive: false, providerDisplayName: 'bar', }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts index 0cb027d1beb..f8dae486736 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts @@ -3,13 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; import { assertSnapshot } from 'vs/base/test/common/snapshot'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IEditorPaneSelectionChangeEvent } from 'vs/workbench/common/editor'; import { NotebookCellOutline } from 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import { INotebookEditor, INotebookEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; @@ -29,23 +32,25 @@ suite('NotebookEditorStickyScroll', () => { disposables.dispose(); }); - ensureNoDisposablesAreLeakedInTestSuite(); + const store = ensureNoDisposablesAreLeakedInTestSuite(); setup(() => { disposables = new DisposableStore(); instantiationService = setupInstantiationService(disposables); + instantiationService.set(ILanguageFeaturesService, new LanguageFeaturesService()); }); function getOutline(editor: any) { if (!editor.hasModel()) { assert.ok(false, 'MUST have active text editor'); } - const outline = instantiationService.createInstance(NotebookCellOutline, new class extends mock() { + const outline = store.add(instantiationService.createInstance(NotebookCellOutline, new class extends mock() { override getControl() { return editor; } override onDidChangeModel: Event = Event.None; - }, OutlineTarget.QuickPick); + override onDidChangeSelection: Event = Event.None; + }, OutlineTarget.QuickPick)); return outline; } diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts index 4c250a43ab1..abcb1828caa 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts index fa082d78e23..ee359d70e9d 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { AsyncIterableObject, AsyncIterableSource } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts index f226b1c608a..c9d92f4a71b 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; @@ -66,7 +66,7 @@ suite('NotebookViewModel', () => { test('ctor', function () { const notebook = new NotebookTextModel('notebook', URI.parse('test'), [], {}, { transientCellMetadata: {}, transientDocumentMetadata: {}, transientOutputs: false, cellContentMetadata: {} }, undoRedoService, modelService, languageService, languageDetectionService); const model = new NotebookEditorTestModel(notebook); - const options = new NotebookOptions(mainWindow, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService), false); + const options = new NotebookOptions(mainWindow, false, undefined, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService)); const eventDispatcher = new NotebookEventDispatcher(); const viewContext = new ViewContext(options, eventDispatcher, () => ({} as IBaseCellEditorOptions)); const viewModel = new NotebookViewModel('notebook', model.notebook, viewContext, null, { isReadOnly: false }, instantiationService, bulkEditService, undoRedoService, textModelService, notebookExecutionStateService); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts index f2af8c32747..199ee022a90 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookWorkbenchToolbar.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookWorkbenchToolbar.test.ts index 34c0a8c1a01..8d530e9c21c 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookWorkbenchToolbar.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookWorkbenchToolbar.test.ts @@ -5,7 +5,7 @@ import { workbenchCalculateActions, workbenchDynamicCalculateActions } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar'; import { Action, IAction, Separator } from 'vs/base/common/actions'; -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface IActionModel { diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index 6db24ac2b10..830ac41e6a8 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -52,7 +52,7 @@ import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/vie import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { CellKind, CellUri, ICellDto2, INotebookDiffEditorModel, INotebookEditorModel, INotebookSearchOptions, IOutputDto, IResolvedNotebookEditorModel, NotebookCellExecutionState, NotebookCellMetadata, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri, ICellDto2, INotebookDiffEditorModel, INotebookEditorModel, INotebookFindOptions, IOutputDto, IResolvedNotebookEditorModel, NotebookCellExecutionState, NotebookCellMetadata, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellExecuteUpdate, ICellExecutionComplete, ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookCellExecution, INotebookExecution, INotebookExecutionStateService, INotebookFailStateChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; @@ -65,7 +65,7 @@ import { EditorFontLigatures, EditorFontVariations } from 'vs/editor/common/conf import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { mainWindow } from 'vs/base/browser/window'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; -import { INotebookCellOutlineProviderFactory, NotebookCellOutlineProviderFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory'; +import { INotebookCellOutlineDataSourceFactory, NotebookCellOutlineDataSourceFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory'; import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; export class TestCell extends NotebookCellTextModel { @@ -199,7 +199,7 @@ export function setupInstantiationService(disposables: DisposableStore) { instantiationService.stub(IKeybindingService, new MockKeybindingService()); instantiationService.stub(INotebookCellStatusBarService, disposables.add(new NotebookCellStatusBarService())); instantiationService.stub(ICodeEditorService, disposables.add(new TestCodeEditorService(testThemeService))); - instantiationService.stub(INotebookCellOutlineProviderFactory, instantiationService.createInstance(NotebookCellOutlineProviderFactory)); + instantiationService.stub(INotebookCellOutlineDataSourceFactory, instantiationService.createInstance(NotebookCellOutlineDataSourceFactory)); instantiationService.stub(ILanguageDetectionService, new class MockLanguageDetectionService implements ILanguageDetectionService { _serviceBrand: undefined; @@ -229,8 +229,9 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic }), {}, { transientCellMetadata: {}, transientDocumentMetadata: {}, cellContentMetadata: {}, transientOutputs: false })); const model = disposables.add(new NotebookEditorTestModel(notebook)); - const notebookOptions = disposables.add(new NotebookOptions(mainWindow, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService), false)); - const viewContext = new ViewContext(notebookOptions, disposables.add(new NotebookEventDispatcher()), () => ({} as IBaseCellEditorOptions)); + const notebookOptions = disposables.add(new NotebookOptions(mainWindow, false, undefined, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService))); + const baseCellEditorOptions = new class extends mock() { }; + const viewContext = new ViewContext(notebookOptions, disposables.add(new NotebookEventDispatcher()), () => baseCellEditorOptions); const viewModel: NotebookViewModel = disposables.add(instantiationService.createInstance(NotebookViewModel, viewType, model.notebook, viewContext, null, { isReadOnly: false })); const cellList = disposables.add(createNotebookCellList(instantiationService, disposables, viewContext)); @@ -310,7 +311,7 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic override get onDidChangeSelection() { return viewModel.onDidChangeSelection as Event; } override get onDidChangeOptions() { return viewModel.onDidChangeOptions; } override get onDidChangeViewCells() { return viewModel.onDidChangeViewCells; } - override async find(query: string, options: INotebookSearchOptions): Promise { + override async find(query: string, options: INotebookFindOptions): Promise { const findMatches = viewModel.find(query, options).filter(match => match.length > 0); return findMatches; } @@ -461,15 +462,16 @@ export function createNotebookCellList(instantiationService: TestInstantiationSe getTemplateId() { return 'template'; } }; + const baseCellRenderTemplate = new class extends mock() { }; const renderer: IListRenderer = { templateId: 'template', - renderTemplate() { return {} as BaseCellRenderTemplate; }, + renderTemplate() { return baseCellRenderTemplate; }, renderElement() { }, disposeTemplate() { } }; const notebookOptions = !!viewContext ? viewContext.notebookOptions - : disposables.add(new NotebookOptions(mainWindow, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService), false)); + : disposables.add(new NotebookOptions(mainWindow, false, undefined, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService))); const cellList: NotebookCellList = disposables.add(instantiationService.createInstance( NotebookCellList, 'NotebookCellList', diff --git a/src/vs/workbench/contrib/outline/browser/outline.contribution.ts b/src/vs/workbench/contrib/outline/browser/outline.contribution.ts index 0a21d1d05bd..794c8bdfdd3 100644 --- a/src/vs/workbench/contrib/outline/browser/outline.contribution.ts +++ b/src/vs/workbench/contrib/outline/browser/outline.contribution.ts @@ -65,17 +65,17 @@ Registry.as(ConfigurationExtensions.Configuration).regis 'default': 'alwaysExpand' }, [OutlineConfigKeys.problemsEnabled]: { - 'markdownDescription': localize('outline.showProblem', "Show errors and warnings on Outline elements. Overwritten by `#problems.visibility#` when it is off."), + 'markdownDescription': localize('outline.showProblem', "Show errors and warnings on Outline elements. Overwritten by {0} when it is off.", '`#problems.visibility#`'), 'type': 'boolean', 'default': true }, [OutlineConfigKeys.problemsColors]: { - 'markdownDescription': localize('outline.problem.colors', "Use colors for errors and warnings on Outline elements. Overwritten by `#problems.visibility#` when it is off."), + 'markdownDescription': localize('outline.problem.colors', "Use colors for errors and warnings on Outline elements. Overwritten by {0} when it is off.", '`#problems.visibility#`'), 'type': 'boolean', 'default': true }, [OutlineConfigKeys.problemsBadges]: { - 'markdownDescription': localize('outline.problems.badges', "Use badges for errors and warnings on Outline elements. Overwritten by `#problems.visibility#` when it is off."), + 'markdownDescription': localize('outline.problems.badges', "Use badges for errors and warnings on Outline elements. Overwritten by {0} when it is off.", '`#problems.visibility#`'), 'type': 'boolean', 'default': true }, diff --git a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts index ca06b37e21f..3b82ad1889e 100644 --- a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts +++ b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts @@ -11,11 +11,11 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { OUTPUT_MODE_ID, LOG_MODE_ID } from 'vs/workbench/services/output/common/output'; import { MonacoWebWorker, createWebWorker } from 'vs/editor/browser/services/webWorker'; import { ICreateData, OutputLinkComputer } from 'vs/workbench/contrib/output/common/outputLinkComputer'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -export class OutputLinkProvider { +export class OutputLinkProvider extends Disposable { private static readonly DISPOSE_WORKER_TIME = 3 * 60 * 1000; // dispose worker after 3 minutes of inactivity @@ -29,6 +29,8 @@ export class OutputLinkProvider { @ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, ) { + super(); + this.disposeWorkerScheduler = new RunOnceScheduler(() => this.disposeWorker(), OutputLinkProvider.DISPOSE_WORKER_TIME); this.registerListeners(); @@ -36,7 +38,7 @@ export class OutputLinkProvider { } private registerListeners(): void { - this.contextService.onDidChangeWorkspaceFolders(() => this.updateLinkProviderWorker()); + this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.updateLinkProviderWorker())); } private updateLinkProviderWorker(): void { diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index eb83c902631..6521eeec891 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -103,8 +103,8 @@ export class OutputService extends Disposable implements IOutputService, ITextMo this.activeOutputChannelLevelIsDefaultContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT.bindTo(contextKeyService); // Register as text model content provider for output - textModelResolverService.registerTextModelContentProvider(OUTPUT_SCHEME, this); - instantiationService.createInstance(OutputLinkProvider); + this._register(textModelResolverService.registerTextModelContentProvider(OUTPUT_SCHEME, this)); + this._register(instantiationService.createInstance(OutputLinkProvider)); // Create output channels for already registered channels const registry = Registry.as(Extensions.OutputChannels); diff --git a/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts b/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts index 2f7988f5236..722c80940c9 100644 --- a/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts +++ b/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { isMacintosh, isLinux, isWindows } from 'vs/base/common/platform'; import { OutputLinkComputer } from 'vs/workbench/contrib/output/common/outputLinkComputer'; diff --git a/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts b/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts index 200f6380244..028a744f38a 100644 --- a/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts +++ b/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts @@ -179,11 +179,9 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo // Major/Minor GC Events case 'MinorGC': minorGCs++; + break; case 'MajorGC': majorGCs++; - if (event.args && typeof event.args.usedHeapSizeAfter === 'number' && typeof event.args.usedHeapSizeBefore === 'number') { - garbage += (event.args.usedHeapSizeBefore - event.args.usedHeapSizeAfter); - } break; // GC Events that block the main thread @@ -193,6 +191,12 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo duration += event.dur; break; } + + if (event.name === 'MajorGC' || event.name === 'MinorGC') { + if (typeof event.args?.usedHeapSizeAfter === 'number' && typeof event.args.usedHeapSizeBefore === 'number') { + garbage += (event.args.usedHeapSizeBefore - event.args.usedHeapSizeAfter); + } + } } return { minorGCs, majorGCs, used, garbage, duration: Math.round(duration / 1000) }; diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts index 5deef7c729c..621b1d9057a 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts @@ -295,10 +295,11 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot this._notebookOptions = this._creationOptions?.options ?? new NotebookOptions( DOM.getActiveWindow(), + this.isReadOnly, + undefined, this.configurationService, this.notebookExecutionStateService, - this._codeEditorService, - this.isReadOnly + this._codeEditorService ); return this._notebookOptions; diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 2c8e669e7a0..6f3c5086186 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -60,7 +60,7 @@ import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetN import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; const $ = DOM.$; @@ -903,7 +903,7 @@ class ActionsColumnRenderer implements ITableRenderer(extensionContainer, $('a.extension-label', { tabindex: 0 })); @@ -1213,7 +1213,7 @@ class WhenColumnRenderer implements ITableRenderer { const foregroundColor = theme.getColor(foreground); diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index fa4dd5b81d4..21d3440c8e4 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -408,7 +408,8 @@ } .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title .setting-item-overrides a.modified-scope { - text-decoration: underline; + color: var(--vscode-textLink-foreground); + text-decoration: var(--text-link-decoration); cursor: pointer; } @@ -512,6 +513,11 @@ color: var(--vscode-textLink-foreground); } +.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 { + text-decoration: var(--text-link-decoration); +} + .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a:focus, .settings-editor > .settings-body .settings-tree-container .setting-item-contents .edit-in-settings-button:focus { outline: 1px solid -webkit-focus-ring-color; diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 66ca47c40e5..ccf4663736c 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -1246,6 +1246,7 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo ResourceContextKey.Resource.isEqualTo(this.userDataProfilesService.defaultProfile.settingsResource.toString())), ContextKeyExpr.not('isInDiffEditor')); const registerOpenUserSettingsEditorFromJsonAction = () => { + registerOpenUserSettingsEditorFromJsonActionDisposables.value = undefined; registerOpenUserSettingsEditorFromJsonActionDisposables.value = registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 1ce23e61678..dc76d156be6 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -500,6 +500,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc this._register(this.editor.getModel()!.onDidChangeContent(() => this.delayedRender())); this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.source === ConfigurationTarget.DEFAULT)(() => this.delayedRender())); this._register(languageFeaturesService.codeActionProvider.register({ pattern: settingsEditorModel.uri.path }, this)); + this._register(userDataProfileService.onDidChangeCurrentProfile(() => this.delayedRender())); } private delayedRender(): void { diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 585b89b4014..4e68e88dc97 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -35,7 +35,7 @@ import { settingsEditIcon, settingsScopeDropDownIcon } from 'vs/workbench/contri import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; export class FolderSettingsActionViewItem extends BaseActionViewItem { @@ -45,7 +45,7 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { private container!: HTMLElement; private anchorElement!: HTMLElement; - private anchorElementHover!: IUpdatableHover; + private anchorElementHover!: IManagedHover; private labelElement!: HTMLElement; private detailsElement!: HTMLElement; private dropDownElement!: HTMLElement; @@ -93,7 +93,7 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { 'aria-haspopup': 'true', 'tabindex': '0' }, this.labelElement, this.detailsElement, this.dropDownElement); - this.anchorElementHover = this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.anchorElement, '')); + this.anchorElementHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.anchorElement, '')); this._register(DOM.addDisposableListener(this.anchorElement, DOM.EventType.MOUSE_DOWN, e => DOM.EventHelper.stop(e))); this._register(DOM.addDisposableListener(this.anchorElement, DOM.EventType.CLICK, e => this.onClick(e))); this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, e => this.onKeyUp(e))); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 0b0441c9356..f9b11cf76a8 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -17,7 +17,7 @@ import { isCancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose, type IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/settingsEditor2'; @@ -219,6 +219,8 @@ export class SettingsEditor2 extends EditorPane { private installedExtensionIds: string[] = []; + private readonly inputChangeListener: MutableDisposable; + constructor( group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @@ -289,6 +291,7 @@ export class SettingsEditor2 extends EditorPane { if (ENABLE_LANGUAGE_FILTER && !SettingsEditor2.SUGGESTIONS.includes(`@${LANGUAGE_SETTING_TAG}`)) { SettingsEditor2.SUGGESTIONS.push(`@${LANGUAGE_SETTING_TAG}`); } + this.inputChangeListener = this._register(new MutableDisposable()); } override get minimumWidth(): number { return SettingsEditor2.EDITOR_MIN_WIDTH; } @@ -382,9 +385,9 @@ export class SettingsEditor2 extends EditorPane { // Don't block setInput on render (which can trigger an async search) this.onConfigUpdate(undefined, true).then(() => { - this._register(input.onWillDispose(() => { + this.inputChangeListener.value = input.onWillDispose(() => { this.searchWidget.setValue(''); - })); + }); // Init TOC selection this.updateTreeScrollSync(); @@ -791,10 +794,10 @@ export class SettingsEditor2 extends EditorPane { this.createTOC(this.tocTreeContainer); this.createSettingsTree(this.settingsTreeContainer); - this.splitView = new SplitView(this.bodyContainer, { + this.splitView = this._register(new SplitView(this.bodyContainer, { orientation: Orientation.HORIZONTAL, proportionalLayout: true - }); + })); const startingWidth = this.storageService.getNumber('settingsEditor2.splitViewWidth', StorageScope.PROFILE, SettingsEditor2.TOC_RESET_WIDTH); this.splitView.addView({ onDidChange: Event.None, @@ -911,7 +914,7 @@ export class SettingsEditor2 extends EditorPane { } private createSettingsTree(container: HTMLElement): void { - this.settingRenderers = this.instantiationService.createInstance(SettingTreeRenderers); + this.settingRenderers = this._register(this.instantiationService.createInstance(SettingTreeRenderers)); this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type, e.manualReset, e.scope))); this._register(this.settingRenderers.onDidOpenSettings(settingKey => { this.openSettingsFile({ revealSetting: { key: settingKey, edit: true } }); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index 6db2e7883d9..59b31491a53 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts @@ -9,7 +9,7 @@ import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Emitter } from 'vs/base/common/event'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -448,15 +448,27 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { updateDefaultOverrideIndicator(element: SettingsTreeSettingElement) { this.defaultOverrideIndicator.element.style.display = 'none'; - const sourceToDisplay = getDefaultValueSourceToDisplay(element); + let sourceToDisplay = getDefaultValueSourceToDisplay(element); if (sourceToDisplay !== undefined) { this.defaultOverrideIndicator.element.style.display = 'inline'; this.defaultOverrideIndicator.disposables.clear(); - const defaultOverrideHoverContent = localize('defaultOverriddenDetails', "Default setting value overridden by {0}", sourceToDisplay); + // Show source of default value when hovered + if (Array.isArray(sourceToDisplay) && sourceToDisplay.length === 1) { + sourceToDisplay = sourceToDisplay[0]; + } + + let defaultOverrideHoverContent; + if (!Array.isArray(sourceToDisplay)) { + defaultOverrideHoverContent = localize('defaultOverriddenDetails', "Default setting value overridden by `{0}`", sourceToDisplay); + } else { + sourceToDisplay = sourceToDisplay.map(source => `\`${source}\``); + defaultOverrideHoverContent = localize('multipledefaultOverriddenDetails', "A default values has been set by {0}", sourceToDisplay.slice(0, -1).join(', ') + ' & ' + sourceToDisplay.slice(-1)); + } + const showHover = (focus: boolean) => { return this.hoverService.showHover({ - content: defaultOverrideHoverContent, + content: new MarkdownString().appendMarkdown(defaultOverrideHoverContent), target: this.defaultOverrideIndicator.element, position: { hoverPosition: HoverPosition.BELOW, @@ -473,14 +485,22 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { } } -function getDefaultValueSourceToDisplay(element: SettingsTreeSettingElement): string | undefined { - let sourceToDisplay: string | undefined; +function getDefaultValueSourceToDisplay(element: SettingsTreeSettingElement): string | undefined | string[] { + let sourceToDisplay: string | undefined | string[]; const defaultValueSource = element.defaultValueSource; if (defaultValueSource) { - if (typeof defaultValueSource !== 'string') { - sourceToDisplay = defaultValueSource.displayName ?? defaultValueSource.id; + if (defaultValueSource instanceof Map) { + sourceToDisplay = []; + for (const [, value] of defaultValueSource) { + const newValue = typeof value !== 'string' ? value.displayName ?? value.id : value; + if (!sourceToDisplay.includes(newValue)) { + sourceToDisplay.push(newValue); + } + } } else if (typeof defaultValueSource === 'string') { sourceToDisplay = defaultValueSource; + } else { + sourceToDisplay = defaultValueSource.displayName ?? defaultValueSource.id; } } return sourceToDisplay; @@ -538,9 +558,19 @@ export function getIndicatorsLabelAriaLabel(element: SettingsTreeSettingElement, } // Add default override indicator text - const sourceToDisplay = getDefaultValueSourceToDisplay(element); + let sourceToDisplay = getDefaultValueSourceToDisplay(element); if (sourceToDisplay !== undefined) { - ariaLabelSections.push(localize('defaultOverriddenDetailsAriaLabel', "{0} overrides the default value", sourceToDisplay)); + if (Array.isArray(sourceToDisplay) && sourceToDisplay.length === 1) { + sourceToDisplay = sourceToDisplay[0]; + } + + let overriddenDetailsText; + if (!Array.isArray(sourceToDisplay)) { + overriddenDetailsText = localize('defaultOverriddenDetailsAriaLabel', "{0} overrides the default value", sourceToDisplay); + } else { + overriddenDetailsText = localize('multipleDefaultOverriddenDetailsAriaLabel', "{0} override the default value", sourceToDisplay.slice(0, -1).join(', ') + ' & ' + sourceToDisplay.slice(-1)); + } + ariaLabelSections.push(overriddenDetailsText); } // Add text about default values being overridden in other languages diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 80ce14bcaba..62a948e5a08 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -60,8 +60,8 @@ import { settingsMoreActionIcon } from 'vs/workbench/contrib/preferences/browser import { SettingsTarget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { ISettingOverrideClickEvent, SettingsTreeIndicatorsLabel, getIndicatorsLabelAriaLabel } from 'vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators'; import { ITOCEntry } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; -import { ISettingsEditorViewState, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement, inspectSetting, settingKeyToDisplayFormat } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; -import { ExcludeSettingWidget, IListDataItem, IObjectDataItem, IObjectEnumOption, IObjectKeySuggester, IObjectValueSuggester, ISettingListChangeEvent, IncludeSettingWidget, ListSettingWidget, ObjectSettingCheckboxWidget, ObjectSettingDropdownWidget, ObjectValue } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; +import { ISettingsEditorViewState, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement, inspectSetting, objectSettingSupportsRemoveDefaultValue, settingKeyToDisplayFormat } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; +import { ExcludeSettingWidget, IIncludeExcludeDataItem, IListDataItem, IObjectDataItem, IObjectEnumOption, IObjectKeySuggester, IObjectValueSuggester, ISettingListChangeEvent, IncludeSettingWidget, ListSettingWidget, ObjectSettingCheckboxWidget, ObjectSettingDropdownWidget, ObjectValue, SettingListEvent } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { LANGUAGE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, compareTwoNullableNumbers } from 'vs/workbench/contrib/preferences/common/preferences'; import { settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; @@ -74,14 +74,27 @@ import { IHoverService } from 'vs/platform/hover/browser/hover'; const $ = DOM.$; -function getIncludeExcludeDisplayValue(element: SettingsTreeSettingElement): IListDataItem[] { +function getIncludeExcludeDisplayValue(element: SettingsTreeSettingElement): IIncludeExcludeDataItem[] { + const elementDefaultValue: Record = typeof element.defaultValue === 'object' + ? element.defaultValue ?? {} + : {}; + const data = element.isConfigured ? - { ...element.defaultValue, ...element.scopeValue } : - element.defaultValue; + { ...elementDefaultValue, ...element.scopeValue } : + elementDefaultValue; return Object.keys(data) .filter(key => !!data[key]) .map(key => { + const defaultValue = elementDefaultValue[key]; + + // Get source if it's a default value + let source: string | undefined; + if (defaultValue === data[key] && element.setting.type === 'object' && element.defaultValueSource instanceof Map) { + const defaultSource = element.defaultValueSource.get(key); + source = typeof defaultSource === 'string' ? defaultSource : defaultSource?.displayName; + } + const value = data[key]; const sibling = typeof value === 'boolean' ? undefined : value.when; return { @@ -90,7 +103,8 @@ function getIncludeExcludeDisplayValue(element: SettingsTreeSettingElement): ILi data: key }, sibling, - elementType: element.valueType + elementType: element.valueType, + source }; }); } @@ -162,6 +176,14 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData return Object.keys(data).map(key => { const defaultValue = elementDefaultValue[key]; + + // Get source if it's a default value + let source: string | undefined; + if (defaultValue === data[key] && element.setting.type === 'object' && element.defaultValueSource instanceof Map) { + const defaultSource = element.defaultValueSource.get(key); + source = typeof defaultSource === 'string' ? defaultSource : defaultSource?.displayName; + } + if (isDefined(objectProperties) && key in objectProperties) { if (element.setting.allKeysAreBoolean) { return { @@ -174,7 +196,9 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData data: data[key] }, keyDescription: objectProperties[key].description, - removable: false + removable: false, + resetable: true, + source } as IObjectDataItem; } @@ -192,12 +216,15 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData }, keyDescription: objectProperties[key].description, removable: isUndefinedOrNull(defaultValue), + resetable: !isUndefinedOrNull(defaultValue), + source } as IObjectDataItem; } - // The row is removable if it doesn't have a default value assigned. - // Otherwise, it is not removable, but its value can be reset to the default. - const removable = !defaultValue; + // The row is removable if it doesn't have a default value assigned or the setting supports removing the default value. + // If a default value is assigned and the user modified the default, it can be reset back to the default. + const removable = defaultValue === undefined || objectSettingSupportsRemoveDefaultValue(element.setting.key); + const resetable = defaultValue && defaultValue !== data[key]; const schema = patternsAndSchemas.find(({ pattern }) => pattern.test(key))?.schema; if (schema) { const valueEnumOptions = getEnumOptionsFromSchema(schema); @@ -210,6 +237,8 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData }, keyDescription: schema.description, removable, + resetable, + source } as IObjectDataItem; } @@ -228,6 +257,8 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData }, keyDescription: typeof objectAdditionalProperties === 'object' ? objectAdditionalProperties.description : undefined, removable, + resetable, + source } as IObjectDataItem; }).filter(item => !isUndefinedOrNull(item.value.data)); } @@ -629,12 +660,12 @@ interface ISettingComplexItemTemplate extends ISettingItemTemplate { } interface ISettingListItemTemplate extends ISettingItemTemplate { - listWidget: ListSettingWidget; + listWidget: ListSettingWidget; validationErrorMessageElement: HTMLElement; } interface ISettingIncludeExcludeItemTemplate extends ISettingItemTemplate { - includeExcludeWidget: ListSettingWidget; + includeExcludeWidget: ListSettingWidget; } interface ISettingObjectItemTemplate extends ISettingItemTemplate | undefined> { @@ -805,7 +836,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const descriptionElement = DOM.append(container, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); - toDispose.add(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, () => localize('modified', "The setting has been configured in the current scope."))); + toDispose.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, () => localize('modified', "The setting has been configured in the current scope."))); const valueElement = DOM.append(container, $('.setting-item-value')); const controlElement = DOM.append(valueElement, $('div.setting-item-control')); @@ -892,7 +923,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const titleTooltip = setting.key + (element.isConfigured ? ' - Modified' : ''); template.categoryElement.textContent = element.displayCategory ? (element.displayCategory + ': ') : ''; - template.elementDisposables.add(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), template.categoryElement, titleTooltip)); + template.elementDisposables.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.categoryElement, titleTooltip)); template.labelElement.text = element.displayLabel; template.labelElement.title = titleTooltip; @@ -1174,7 +1205,7 @@ class SettingArrayRenderer extends AbstractSettingRenderer implements ITreeRende return template; } - private computeNewList(template: ISettingListItemTemplate, e: ISettingListChangeEvent): string[] | undefined { + private computeNewList(template: ISettingListItemTemplate, e: SettingListEvent): string[] | undefined { if (template.context) { let newValue: string[] = []; if (Array.isArray(template.context.scopeValue)) { @@ -1183,33 +1214,28 @@ class SettingArrayRenderer extends AbstractSettingRenderer implements ITreeRende newValue = [...template.context.value]; } - if (e.sourceIndex !== undefined) { + if (e.type === 'move') { // A drag and drop occurred const sourceIndex = e.sourceIndex; - const targetIndex = e.targetIndex!; + const targetIndex = e.targetIndex; const splicedElem = newValue.splice(sourceIndex, 1)[0]; newValue.splice(targetIndex, 0, splicedElem); - } else if (e.targetIndex !== undefined) { - const itemValueData = e.item?.value.data.toString() ?? ''; - // Delete value - if (!e.item?.value.data && e.originalItem.value.data && e.targetIndex > -1) { - newValue.splice(e.targetIndex, 1); - } + } else if (e.type === 'remove' || e.type === 'reset') { + newValue.splice(e.targetIndex, 1); + } else if (e.type === 'change') { + const itemValueData = e.newItem.value.data.toString(); + // Update value - else if (e.item?.value.data && e.originalItem.value.data) { - if (e.targetIndex > -1) { - newValue[e.targetIndex] = itemValueData; - } - // For some reason, we are updating and cannot find original value - // Just append the value in this case - else { - newValue.push(itemValueData); - } + if (e.targetIndex > -1) { + newValue[e.targetIndex] = itemValueData; } - // Add value - else if (e.item?.value.data && !e.originalItem.value.data && e.targetIndex >= newValue.length) { + // For some reason, we are updating and cannot find original value + // Just append the value in this case + else { newValue.push(itemValueData); } + } else if (e.type === 'add') { + newValue.push(e.newItem.value.data.toString()); } if ( @@ -1288,9 +1314,10 @@ abstract class AbstractSettingObjectRenderer extends AbstractSettingRenderer imp return template; } - protected onDidChangeObject(template: ISettingObjectItemTemplate, e: ISettingListChangeEvent): void { + protected onDidChangeObject(template: ISettingObjectItemTemplate, e: SettingListEvent): void { const widget = (template.objectCheckboxWidget ?? template.objectDropdownWidget)!; if (template.context) { + const settingSupportsRemoveDefault = objectSettingSupportsRemoveDefaultValue(template.context.setting.key); const defaultValue: Record = typeof template.context.defaultValue === 'object' ? template.context.defaultValue ?? {} : {}; @@ -1299,45 +1326,55 @@ abstract class AbstractSettingObjectRenderer extends AbstractSettingRenderer imp ? template.context.scopeValue ?? {} : {}; - const newValue: Record = {}; + const newValue: Record = { ...template.context.scopeValue }; // Initialize with scoped values as removed default values are not rendered const newItems: IObjectDataItem[] = []; widget.items.forEach((item, idx) => { // Item was updated - if (isDefined(e.item) && e.targetIndex === idx) { - newValue[e.item.key.data] = e.item.value.data; - newItems.push(e.item); + if ((e.type === 'change' || e.type === 'move') && e.targetIndex === idx) { + // If the key of the default value is changed, remove the default value + if (e.originalItem.key.data !== e.newItem.key.data && settingSupportsRemoveDefault && e.originalItem.key.data in defaultValue) { + newValue[e.originalItem.key.data] = null; + } + newValue[e.newItem.key.data] = e.newItem.value.data; + newItems.push(e.newItem); } // All remaining items, but skip the one that we just updated - else if (isUndefinedOrNull(e.item) || e.item.key.data !== item.key.data) { + else if ((e.type !== 'change' && e.type !== 'move') || e.newItem.key.data !== item.key.data) { newValue[item.key.data] = item.value.data; newItems.push(item); } }); // Item was deleted - if (isUndefinedOrNull(e.item)) { - delete newValue[e.originalItem.key.data]; + if (e.type === 'remove' || e.type === 'reset') { + const objectKey = e.originalItem.key.data; + const removingDefaultValue = e.type === 'remove' && settingSupportsRemoveDefault && defaultValue[objectKey] === e.originalItem.value.data; + if (removingDefaultValue) { + newValue[objectKey] = null; + } else { + delete newValue[objectKey]; + } - const itemToDelete = newItems.findIndex(item => item.key.data === e.originalItem.key.data); - const defaultItemValue = defaultValue[e.originalItem.key.data] as string | boolean; + const itemToDelete = newItems.findIndex(item => item.key.data === objectKey); + const defaultItemValue = defaultValue[objectKey] as string | boolean; - // Item does not have a default - if (isUndefinedOrNull(defaultValue[e.originalItem.key.data]) && itemToDelete > -1) { + // Item does not have a default or default is bing removed + if (removingDefaultValue || isUndefinedOrNull(defaultValue[objectKey]) && itemToDelete > -1) { newItems.splice(itemToDelete, 1); - } else if (itemToDelete > -1) { + } else if (!removingDefaultValue && itemToDelete > -1) { newItems[itemToDelete].value.data = defaultItemValue; } } // New item was added - else if (widget.isItemNew(e.originalItem) && e.item.key.data !== '') { - newValue[e.item.key.data] = e.item.value.data; - newItems.push(e.item); + else if (e.type === 'add') { + newValue[e.newItem.key.data] = e.newItem.value.data; + newItems.push(e.newItem); } Object.entries(newValue).forEach(([key, value]) => { // value from the scope has changed back to the default - if (scopeValue[key] !== value && defaultValue[key] === value) { + if (scopeValue[key] !== value && defaultValue[key] === value && !(settingSupportsRemoveDefault && value === null)) { delete newValue[key]; } }); @@ -1462,25 +1499,27 @@ abstract class SettingIncludeExcludeRenderer extends AbstractSettingRenderer imp return template; } - private onDidChangeIncludeExclude(template: ISettingIncludeExcludeItemTemplate, e: ISettingListChangeEvent): void { + private onDidChangeIncludeExclude(template: ISettingIncludeExcludeItemTemplate, e: SettingListEvent): void { if (template.context) { const newValue = { ...template.context.scopeValue }; // first delete the existing entry, if present - if (e.originalItem.value.data.toString() in template.context.defaultValue) { - // delete a default by overriding it - newValue[e.originalItem.value.data.toString()] = false; - } else { - delete newValue[e.originalItem.value.data.toString()]; + if (e.type !== 'add') { + if (e.originalItem.value.data.toString() in template.context.defaultValue) { + // delete a default by overriding it + newValue[e.originalItem.value.data.toString()] = false; + } else { + delete newValue[e.originalItem.value.data.toString()]; + } } // then add the new or updated entry, if present - if (e.item?.value) { - if (e.item.value.data.toString() in template.context.defaultValue && !e.item.sibling) { + if (e.type === 'change' || e.type === 'add' || e.type === 'move') { + if (e.newItem.value.data.toString() in template.context.defaultValue && !e.newItem.sibling) { // add a default by deleting its override - delete newValue[e.item.value.data.toString()]; + delete newValue[e.newItem.value.data.toString()]; } else { - newValue[e.item.value.data.toString()] = e.item.sibling ? { when: e.item.sibling } : true; + newValue[e.newItem.value.data.toString()] = e.newItem.sibling ? { when: e.newItem.sibling } : true; } } @@ -1698,7 +1737,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre const enumDescriptionsAreMarkdown = dataElement.setting.enumDescriptionsAreMarkdown; const disposables = new DisposableStore(); - template.toDispose.add(disposables); + template.elementDisposables.add(disposables); let createdDefault = false; if (!settingEnum.includes(dataElement.defaultValue)) { @@ -1835,7 +1874,7 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control')); const descriptionElement = DOM.append(descriptionAndValueElement, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); - toDispose.add(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, localize('modified', "The setting has been configured in the current scope."))); + toDispose.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, localize('modified', "The setting has been configured in the current scope."))); const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); @@ -1946,10 +1985,10 @@ export class SettingsExtensionToggleRenderer extends AbstractSettingRenderer imp } } -export class SettingTreeRenderers { +export class SettingTreeRenderers extends Disposable { readonly onDidClickOverrideElement: Event; - private readonly _onDidChangeSetting = new Emitter(); + private readonly _onDidChangeSetting = this._register(new Emitter()); readonly onDidChangeSetting: Event; readonly onDidOpenSettings: Event; @@ -1973,6 +2012,7 @@ export class SettingTreeRenderers { @IUserDataProfilesService private readonly _userDataProfilesService: IUserDataProfilesService, @IUserDataSyncEnablementService private readonly _userDataSyncEnablementService: IUserDataSyncEnablementService, ) { + super(); this.settingActions = [ new Action('settings.resetSetting', localize('resetSettingLabel', "Reset Setting"), undefined, undefined, async context => { if (context instanceof SettingsTreeSettingElement) { @@ -2078,6 +2118,20 @@ export class SettingTreeRenderers { const settingElement = this.getSettingDOMElementForDOMElement(element); return settingElement && settingElement.getAttribute(AbstractSettingRenderer.SETTING_ID_ATTR); } + + override dispose(): void { + super.dispose(); + this.settingActions.forEach(action => { + if (isDisposable(action)) { + action.dispose(); + } + }); + this.allRenderers.forEach(renderer => { + if (isDisposable(renderer)) { + renderer.dispose(); + } + }); + } } /** @@ -2134,7 +2188,7 @@ function cleanRenderedMarkdown(element: Node): void { const tagName = (child).tagName && (child).tagName.toLowerCase(); if (tagName === 'img') { - element.removeChild(child); + child.remove(); } else { cleanRenderedMarkdown(child); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index e30c3bb549a..e651ffdccf7 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -17,7 +17,7 @@ import { FOLDER_SCOPES, WORKSPACE_SCOPES, REMOTE_MACHINE_SCOPES, LOCAL_MACHINE_S import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; -import { ConfigurationScope, EditPresentationTypes, Extensions, IConfigurationRegistry, IExtensionInfo } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationDefaultValueSource, ConfigurationScope, EditPresentationTypes, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { Registry } from 'vs/platform/registry/common/platform'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; @@ -135,7 +135,7 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { * The source of the default value to display. * This value also accounts for extension-contributed language-specific default value overrides. */ - defaultValueSource: string | IExtensionInfo | undefined; + defaultValueSource: ConfigurationDefaultValueSource | undefined; /** * Whether the setting is configured in the selected scope. @@ -792,11 +792,25 @@ function isIncludeSetting(setting: ISetting): boolean { return setting.key === 'files.readonlyInclude'; } -function isObjectRenderableSchema({ type }: IJSONSchema): boolean { - return type === 'string' || type === 'boolean' || type === 'integer' || type === 'number'; +// The values of the following settings when a default values has been removed +export function objectSettingSupportsRemoveDefaultValue(key: string): boolean { + return key === 'workbench.editor.customLabels.patterns'; +} + +function isObjectRenderableSchema({ type }: IJSONSchema, key: string): boolean { + if (type === 'string' || type === 'boolean' || type === 'integer' || type === 'number') { + return true; + } + + if (objectSettingSupportsRemoveDefaultValue(key) && Array.isArray(type) && type.length === 2) { + return type.includes('null') && (type.includes('string') || type.includes('boolean') || type.includes('integer') || type.includes('number')); + } + + return false; } function isObjectSetting({ + key, type, objectProperties, objectPatternProperties, @@ -838,7 +852,7 @@ function isObjectSetting({ return [schema]; }).flat(); - return flatSchemas.every(isObjectRenderableSchema); + return flatSchemas.every((schema) => isObjectRenderableSchema(schema, key)); } function settingTypeEnumRenderable(_type: string | string[]) { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index c2f0f7b7759..9e9dcc17e86 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -29,6 +29,9 @@ import { settingsSelectBackground, settingsSelectBorder, settingsSelectForegroun import { defaultButtonStyles, getInputBoxStyle, getSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import { SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; const $ = DOM.$; @@ -110,21 +113,49 @@ export class ListSettingListModel { } export interface ISettingListChangeEvent { + type: 'change'; originalItem: TDataItem; - item?: TDataItem; - targetIndex?: number; - sourceIndex?: number; + newItem: TDataItem; + targetIndex: number; } +export interface ISettingListAddEvent { + type: 'add'; + newItem: TDataItem; + targetIndex: number; +} + +export interface ISettingListMoveEvent { + type: 'move'; + originalItem: TDataItem; + newItem: TDataItem; + targetIndex: number; + sourceIndex: number; +} + +export interface ISettingListRemoveEvent { + type: 'remove'; + originalItem: TDataItem; + targetIndex: number; +} + +export interface ISettingListResetEvent { + type: 'reset'; + originalItem: TDataItem; + targetIndex: number; +} + +export type SettingListEvent = ISettingListChangeEvent | ISettingListAddEvent | ISettingListMoveEvent | ISettingListRemoveEvent | ISettingListResetEvent; + export abstract class AbstractListSettingWidget extends Disposable { private listElement: HTMLElement; private rowElements: HTMLElement[] = []; - protected readonly _onDidChangeList = this._register(new Emitter>()); + protected readonly _onDidChangeList = this._register(new Emitter>()); protected readonly model = new ListSettingListModel(this.getEmptyItem()); protected readonly listDisposables = this._register(new DisposableStore()); - readonly onDidChangeList: Event> = this._onDidChangeList.event; + readonly onDidChangeList: Event> = this._onDidChangeList.event; get domNode(): HTMLElement { return this.listElement; @@ -250,11 +281,20 @@ export abstract class AbstractListSettingWidget extend protected handleItemChange(originalItem: TDataItem, changedItem: TDataItem, idx: number) { this.model.setEditKey('none'); - this._onDidChangeList.fire({ - originalItem, - item: changedItem, - targetIndex: idx, - }); + if (this.isItemNew(originalItem)) { + this._onDidChangeList.fire({ + type: 'add', + newItem: changedItem, + targetIndex: idx, + }); + } else { + this._onDidChangeList.fire({ + type: 'change', + originalItem, + newItem: changedItem, + targetIndex: idx, + }); + } this.renderList(); } @@ -396,17 +436,17 @@ export interface IListDataItem { sibling?: string; } -interface ListSettingWidgetDragDetails { +interface ListSettingWidgetDragDetails { element: HTMLElement; - item: IListDataItem; + item: TListDataItem; itemIndex: number; } -export class ListSettingWidget extends AbstractListSettingWidget { +export class ListSettingWidget extends AbstractListSettingWidget { private keyValueSuggester: IObjectKeySuggester | undefined; private showAddButton: boolean = true; - override setValue(listData: IListDataItem[], options?: IListSetValueOptions) { + override setValue(listData: TListDataItem[], options?: IListSetValueOptions) { this.keyValueSuggester = options?.keySuggester; this.showAddButton = options?.showAddButton ?? true; super.setValue(listData); @@ -421,13 +461,13 @@ export class ListSettingWidget extends AbstractListSettingWidget super(container, themeService, contextViewService); } - protected getEmptyItem(): IListDataItem { + protected getEmptyItem(): TListDataItem { return { value: { type: 'string', data: '' } - }; + } as TListDataItem; } protected override isAddButtonVisible(): boolean { @@ -438,7 +478,7 @@ export class ListSettingWidget extends AbstractListSettingWidget return ['setting-list-widget']; } - protected getActionsForItem(item: IListDataItem, idx: number): IAction[] { + protected getActionsForItem(item: TListDataItem, idx: number): IAction[] { return [ { class: ThemeIcon.asClassName(settingsEditIcon), @@ -452,20 +492,20 @@ export class ListSettingWidget extends AbstractListSettingWidget enabled: true, id: 'workbench.action.removeListItem', tooltip: this.getLocalizedStrings().deleteActionTooltip, - run: () => this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) + run: () => this._onDidChangeList.fire({ type: 'remove', originalItem: item, targetIndex: idx }) } ] as IAction[]; } - private dragDetails: ListSettingWidgetDragDetails | undefined; + private dragDetails: ListSettingWidgetDragDetails | undefined; - private getDragImage(item: IListDataItem): HTMLElement { + private getDragImage(item: TListDataItem): HTMLElement { const dragImage = $('.monaco-drag-image'); dragImage.textContent = item.value.data; return dragImage; } - protected renderItem(item: IListDataItem, idx: number): RowElementGroup { + protected renderItem(item: TListDataItem, idx: number): RowElementGroup { const rowElement = $('.setting-list-row'); const valueElement = DOM.append(rowElement, $('.setting-list-value')); const siblingElement = DOM.append(rowElement, $('.setting-list-sibling')); @@ -477,7 +517,7 @@ export class ListSettingWidget extends AbstractListSettingWidget return { rowElement, keyElement: valueElement, valueElement: siblingElement }; } - protected addDragAndDrop(rowElement: HTMLElement, item: IListDataItem, idx: number) { + protected addDragAndDrop(rowElement: HTMLElement, item: TListDataItem, idx: number) { if (this.inReadMode) { rowElement.draggable = true; rowElement.classList.add('draggable'); @@ -497,7 +537,7 @@ export class ListSettingWidget extends AbstractListSettingWidget const dragImage = this.getDragImage(item); rowElement.ownerDocument.body.appendChild(dragImage); ev.dataTransfer.setDragImage(dragImage, -10, -10); - setTimeout(() => rowElement.ownerDocument.body.removeChild(dragImage), 0); + setTimeout(() => dragImage.remove(), 0); } })); this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_OVER, (ev) => { @@ -530,9 +570,10 @@ export class ListSettingWidget extends AbstractListSettingWidget counter = 0; if (this.dragDetails.element !== rowElement) { this._onDidChangeList.fire({ + type: 'move', originalItem: this.dragDetails.item, sourceIndex: this.dragDetails.itemIndex, - item, + newItem: item, targetIndex: idx }); } @@ -548,7 +589,7 @@ export class ListSettingWidget extends AbstractListSettingWidget })); } - protected renderEdit(item: IListDataItem, idx: number): HTMLElement { + protected renderEdit(item: TListDataItem, idx: number): HTMLElement { const rowElement = $('.setting-list-edit-row'); let valueInput: InputBox | SelectBox; let currentDisplayValue: string; @@ -580,7 +621,7 @@ export class ListSettingWidget extends AbstractListSettingWidget break; } - const updatedInputBoxItem = (): IListDataItem => { + const updatedInputBoxItem = (): TListDataItem => { const inputBox = valueInput as InputBox; return { value: { @@ -588,16 +629,16 @@ export class ListSettingWidget extends AbstractListSettingWidget data: inputBox.value }, sibling: siblingInput?.value - }; + } as TListDataItem; }; - const updatedSelectBoxItem = (selectedValue: string): IListDataItem => { + const updatedSelectBoxItem = (selectedValue: string): TListDataItem => { return { value: { type: 'enum', data: selectedValue, options: currentEnumOptions ?? [] } - }; + } as TListDataItem; }; const onKeyDown = (e: StandardKeyboardEvent) => { if (e.equals(KeyCode.Enter)) { @@ -674,17 +715,17 @@ export class ListSettingWidget extends AbstractListSettingWidget return rowElement; } - override isItemNew(item: IListDataItem): boolean { + override isItemNew(item: TListDataItem): boolean { return item.value.data === ''; } - protected addTooltipsToRow(rowElementGroup: RowElementGroup, { value, sibling }: IListDataItem) { + protected addTooltipsToRow(rowElementGroup: RowElementGroup, { value, sibling }: TListDataItem) { const title = isUndefinedOrNull(sibling) ? localize('listValueHintLabel', "List item `{0}`", value.data) : localize('listSiblingHintLabel', "List item `{0}` with sibling `${1}`", value.data, sibling); const { rowElement } = rowElementGroup; - this.listDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), rowElement, title)); + this.listDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), rowElement, title)); rowElement.setAttribute('aria-label', title); } @@ -729,22 +770,28 @@ export class ListSettingWidget extends AbstractListSettingWidget } } -export class ExcludeSettingWidget extends ListSettingWidget { +export class ExcludeSettingWidget extends ListSettingWidget { protected override getContainerClasses() { return ['setting-list-include-exclude-widget']; } - protected override addDragAndDrop(rowElement: HTMLElement, item: IListDataItem, idx: number) { + protected override addDragAndDrop(rowElement: HTMLElement, item: IIncludeExcludeDataItem, idx: number) { return; } - protected override addTooltipsToRow(rowElementGroup: RowElementGroup, { value, sibling }: IListDataItem): void { - const title = isUndefinedOrNull(sibling) - ? localize('excludePatternHintLabel', "Exclude files matching `{0}`", value.data) - : localize('excludeSiblingHintLabel', "Exclude files matching `{0}`, only when a file matching `{1}` is present", value.data, sibling); + protected override addTooltipsToRow(rowElementGroup: RowElementGroup, item: IIncludeExcludeDataItem): void { + let title = isUndefinedOrNull(item.sibling) + ? localize('excludePatternHintLabel', "Exclude files matching `{0}`", item.value.data) + : localize('excludeSiblingHintLabel', "Exclude files matching `{0}`, only when a file matching `{1}` is present", item.value.data, item.sibling); + + if (item.source) { + title += localize('excludeIncludeSource', ". Default value provided by `{0}`", item.source); + } + + const markdownTitle = new MarkdownString().appendMarkdown(title); const { rowElement } = rowElementGroup; - this.listDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), rowElement, title)); + this.listDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), rowElement, { markdown: markdownTitle, markdownNotSupportedFallback: title })); rowElement.setAttribute('aria-label', title); } @@ -759,22 +806,28 @@ export class ExcludeSettingWidget extends ListSettingWidget { } } -export class IncludeSettingWidget extends ListSettingWidget { +export class IncludeSettingWidget extends ListSettingWidget { protected override getContainerClasses() { return ['setting-list-include-exclude-widget']; } - protected override addDragAndDrop(rowElement: HTMLElement, item: IListDataItem, idx: number) { + protected override addDragAndDrop(rowElement: HTMLElement, item: IIncludeExcludeDataItem, idx: number) { return; } - protected override addTooltipsToRow(rowElementGroup: RowElementGroup, { value, sibling }: IListDataItem): void { - const title = isUndefinedOrNull(sibling) - ? localize('includePatternHintLabel', "Include files matching `{0}`", value.data) - : localize('includeSiblingHintLabel', "Include files matching `{0}`, only when a file matching `{1}` is present", value.data, sibling); + protected override addTooltipsToRow(rowElementGroup: RowElementGroup, item: IIncludeExcludeDataItem): void { + let title = isUndefinedOrNull(item.sibling) + ? localize('includePatternHintLabel', "Include files matching `{0}`", item.value.data) + : localize('includeSiblingHintLabel', "Include files matching `{0}`, only when a file matching `{1}` is present", item.value.data, item.sibling); + + if (item.source) { + title += localize('excludeIncludeSource', ". Default value provided by `{0}`", item.source); + } + + const markdownTitle = new MarkdownString().appendMarkdown(title); const { rowElement } = rowElementGroup; - this.listDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), rowElement, title)); + this.listDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), rowElement, { markdown: markdownTitle, markdownNotSupportedFallback: title })); rowElement.setAttribute('aria-label', title); } @@ -818,7 +871,16 @@ export interface IObjectDataItem { key: ObjectKey; value: ObjectValue; keyDescription?: string; + source?: string; removable: boolean; + resetable: boolean; +} + +export interface IIncludeExcludeDataItem { + value: ObjectKey; + elementType: SettingValueType; + sibling?: string; + source?: string; } export interface IObjectValueSuggester { @@ -886,6 +948,7 @@ export class ObjectSettingDropdownWidget extends AbstractListSettingWidget this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) + tooltip: this.getLocalizedStrings().resetActionTooltip, + run: () => this._onDidChangeList.fire({ type: 'reset', originalItem: item, targetIndex: idx }) }); - } else { + } + + if (item.removable) { actions.push({ - class: ThemeIcon.asClassName(settingsDiscardIcon), + class: ThemeIcon.asClassName(settingsRemoveIcon), enabled: true, - id: 'workbench.action.resetListItem', + id: 'workbench.action.removeListItem', label: '', - tooltip: this.getLocalizedStrings().resetActionTooltip, - run: () => this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) + tooltip: this.getLocalizedStrings().deleteActionTooltip, + run: () => this._onDidChangeList.fire({ type: 'remove', originalItem: item, targetIndex: idx }) }); } @@ -1181,13 +1246,21 @@ export class ObjectSettingDropdownWidget extends AbstractListSettingWidget; + extensionDescription: IExtensionDescription; }[]> { return (await Promise.all(this.values.map(async (value) => { return { @@ -419,7 +419,7 @@ class IssueReporterItem extends HelpItemBase { label: string; description: string; url: string; - extensionDescription: Readonly; + extensionDescription: IExtensionDescription; }[]> { return Promise.all(this.values.map(async (value) => { return { diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 4e826cecb77..a1b6e3ae2be 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -1562,24 +1562,25 @@ namespace SetTunnelProtocolAction { export const LABEL_HTTP = nls.localize('remote.tunnel.protocolHttp', "HTTP"); export const LABEL_HTTPS = nls.localize('remote.tunnel.protocolHttps', "HTTPS"); - async function handler(arg: any, protocol: TunnelProtocol, remoteExplorerService: IRemoteExplorerService) { + async function handler(arg: any, protocol: TunnelProtocol, remoteExplorerService: IRemoteExplorerService, environmentService: IWorkbenchEnvironmentService) { if (isITunnelItem(arg)) { const attributes: Partial = { protocol }; - return remoteExplorerService.tunnelModel.configPortsAttributes.addAttributes(arg.remotePort, attributes, ConfigurationTarget.USER_REMOTE); + const target = environmentService.remoteAuthority ? ConfigurationTarget.USER_REMOTE : ConfigurationTarget.USER_LOCAL; + return remoteExplorerService.tunnelModel.configPortsAttributes.addAttributes(arg.remotePort, attributes, target); } } export function handlerHttp(): ICommandHandler { return async (accessor, arg) => { - return handler(arg, TunnelProtocol.Http, accessor.get(IRemoteExplorerService)); + return handler(arg, TunnelProtocol.Http, accessor.get(IRemoteExplorerService), accessor.get(IWorkbenchEnvironmentService)); }; } export function handlerHttps(): ICommandHandler { return async (accessor, arg) => { - return handler(arg, TunnelProtocol.Https, accessor.get(IRemoteExplorerService)); + return handler(arg, TunnelProtocol.Https, accessor.get(IRemoteExplorerService), accessor.get(IWorkbenchEnvironmentService)); }; } } @@ -1817,10 +1818,5 @@ MenuRegistry.appendMenuItem(MenuId.TunnelLocalAddressInline, ({ when: isForwardedOrDetectedExpr })); -registerColor('ports.iconRunningProcessForeground', { - light: STATUS_BAR_REMOTE_ITEM_BACKGROUND, - dark: STATUS_BAR_REMOTE_ITEM_BACKGROUND, - hcDark: STATUS_BAR_REMOTE_ITEM_BACKGROUND, - hcLight: STATUS_BAR_REMOTE_ITEM_BACKGROUND -}, nls.localize('portWithRunningProcess.foreground', "The color of the icon for a port that has an associated running process.")); +registerColor('ports.iconRunningProcessForeground', STATUS_BAR_REMOTE_ITEM_BACKGROUND, nls.localize('portWithRunningProcess.foreground', "The color of the icon for a port that has an associated running process.")); diff --git a/src/vs/workbench/contrib/replNotebook/browser/interactiveEditor.css b/src/vs/workbench/contrib/replNotebook/browser/interactiveEditor.css new file mode 100644 index 00000000000..d43c6a6e98b --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/interactiveEditor.css @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.interactive-editor .input-cell-container:focus-within .input-editor-container .monaco-editor { + outline: solid 1px var(--vscode-notebook-focusedCellBorder); +} + +.interactive-editor .input-cell-container .input-editor-container .monaco-editor { + outline: solid 1px var(--vscode-notebook-inactiveFocusedCellBorder); +} + +.interactive-editor .input-cell-container .input-focus-indicator { + top: 8px; +} + +.interactive-editor .input-cell-container .monaco-editor-background, +.interactive-editor .input-cell-container .margin-view-overlays { + background-color: var(--vscode-notebook-cellEditorBackground, var(--vscode-editor-background)); +} diff --git a/src/vs/workbench/contrib/replNotebook/browser/media/interactive.css b/src/vs/workbench/contrib/replNotebook/browser/media/interactive.css new file mode 100644 index 00000000000..f0f5cd4821e --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/media/interactive.css @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.interactive-editor .input-cell-container { + box-sizing: border-box; +} + +.interactive-editor .input-cell-container .input-focus-indicator { + position: absolute; + left: 0px; + height: 19px; +} + +.interactive-editor .input-cell-container .input-focus-indicator::before { + border-left: 3px solid transparent; + border-radius: 2px; + margin-left: 4px; + content: ""; + position: absolute; + width: 0px; + height: 100%; + z-index: 10; + left: 0px; + top: 0px; + height: 100%; +} + +.interactive-editor .input-cell-container .run-button-container { + position: absolute; +} + +.interactive-editor .input-cell-container .run-button-container .monaco-toolbar .actions-container { + justify-content: center; +} diff --git a/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts new file mode 100644 index 00000000000..04826105015 --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; +import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer, IUntypedEditorInput } from 'vs/workbench/common/editor'; +// is one contrib allowed to import from another? +import { parse } from 'vs/base/common/marshalling'; +import { assertType } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { CellEditType, CellKind, NotebookSetting, NotebookWorkingCopyTypeIdentifier, REPL_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorInputOptions } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { ReplEditor } from 'vs/workbench/contrib/replNotebook/browser/replEditor'; +import { ReplEditorInput } from 'vs/workbench/contrib/replNotebook/browser/replEditorInput'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyEditorHandler, IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { extname, isEqual } from 'vs/base/common/resources'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { localize2 } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { Schemas } from 'vs/base/common/network'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; +import { IInteractiveHistoryService } from 'vs/workbench/contrib/interactive/browser/interactiveHistoryService'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +type SerializedNotebookEditorData = { resource: URI; preferredResource: URI; viewType: string; options?: NotebookEditorInputOptions }; +class ReplEditorSerializer implements IEditorSerializer { + canSerialize(input: EditorInput): boolean { + return input.typeId === ReplEditorInput.ID; + } + serialize(input: EditorInput): string { + assertType(input instanceof ReplEditorInput); + const data: SerializedNotebookEditorData = { + resource: input.resource, + preferredResource: input.preferredResource, + viewType: input.viewType, + options: input.options + }; + return JSON.stringify(data); + } + deserialize(instantiationService: IInstantiationService, raw: string) { + const data = parse(raw); + if (!data) { + return undefined; + } + const { resource, viewType } = data; + if (!data || !URI.isUri(resource) || typeof viewType !== 'string') { + return undefined; + } + + const input = instantiationService.createInstance(ReplEditorInput, resource); + return input; + } +} + +Registry.as(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create( + ReplEditor, + REPL_EDITOR_ID, + 'REPL Editor' + ), + [ + new SyncDescriptor(ReplEditorInput) + ] +); + +Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer( + ReplEditorInput.ID, + ReplEditorSerializer +); + +export class ReplDocumentContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.replDocument'; + + constructor( + @INotebookService notebookService: INotebookService, + @IEditorResolverService editorResolverService: IEditorResolverService, + @IEditorService editorService: IEditorService, + @INotebookEditorModelResolverService private readonly notebookEditorModelResolverService: INotebookEditorModelResolverService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(); + + editorResolverService.registerEditor( + `*.replNotebook`, + { + id: 'repl', + label: 'repl Editor', + priority: RegisteredEditorPriority.option + }, + { + canSupportResource: uri => + (uri.scheme === Schemas.untitled && extname(uri) === '.replNotebook') || + (uri.scheme === Schemas.vscodeNotebookCell && extname(uri) === '.replNotebook'), + singlePerResource: true + }, + { + createUntitledEditorInput: async ({ resource, options }) => { + const scratchpad = this.configurationService.getValue(NotebookSetting.InteractiveWindowPromptToSave) !== true; + const ref = await this.notebookEditorModelResolverService.resolve({ untitledResource: resource }, 'jupyter-notebook', { scratchpad }); + + // untitled notebooks are disposed when they get saved. we should not hold a reference + // to such a disposed notebook and therefore dispose the reference as well + ref.object.notebook.onWillDispose(() => { + ref.dispose(); + }); + return { editor: this.instantiationService.createInstance(ReplEditorInput, resource!), options }; + } + } + ); + } +} + +class ReplWindowWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution, IWorkingCopyEditorHandler { + + static readonly ID = 'workbench.contrib.replWorkingCopyEditorHandler'; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService, + @IExtensionService private readonly extensionService: IExtensionService, + ) { + super(); + + this._installHandler(); + } + + handles(workingCopy: IWorkingCopyIdentifier): boolean { + const viewType = this._getViewType(workingCopy); + return !!viewType && viewType === 'jupyter-notebook' && extname(workingCopy.resource) === '.replNotebook'; + + } + + isOpen(workingCopy: IWorkingCopyIdentifier, editor: EditorInput): boolean { + if (!this.handles(workingCopy)) { + return false; + } + + return editor instanceof ReplEditorInput && isEqual(workingCopy.resource, editor.resource); + } + + createEditor(workingCopy: IWorkingCopyIdentifier): EditorInput { + return this.instantiationService.createInstance(ReplEditorInput, workingCopy.resource); + } + + private async _installHandler(): Promise { + await this.extensionService.whenInstalledExtensionsRegistered(); + + this._register(this.workingCopyEditorService.registerHandler(this)); + } + + private _getViewType(workingCopy: IWorkingCopyIdentifier): string | undefined { + return NotebookWorkingCopyTypeIdentifier.parse(workingCopy.typeId); + } +} + +registerWorkbenchContribution2(ReplWindowWorkingCopyEditorHandler.ID, ReplWindowWorkingCopyEditorHandler, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(ReplDocumentContribution.ID, ReplDocumentContribution, WorkbenchPhase.BlockRestore); + + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'repl.newRepl', + title: localize2('repl.editor.open', 'New REPL Editor'), + category: 'Create', + }); + } + + async run(accessor: ServicesAccessor) { + const resource = URI.from({ scheme: Schemas.untitled, path: 'repl.replNotebook' }); + const editorInput: IUntypedEditorInput = { resource, options: { override: 'repl' } }; + + const editorService = accessor.get(IEditorService); + await editorService.openEditor(editorInput, 1); + } +}); + +export async function executeReplInput(accessor: ServicesAccessor, editorControl: { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget }) { + const bulkEditService = accessor.get(IBulkEditService); + const historyService = accessor.get(IInteractiveHistoryService); + const notebookEditorService = accessor.get(INotebookEditorService); + + if (editorControl && editorControl.notebookEditor && editorControl.codeEditor) { + const notebookDocument = editorControl.notebookEditor.textModel; + const textModel = editorControl.codeEditor.getModel(); + const activeKernel = editorControl.notebookEditor.activeKernel; + const language = activeKernel?.supportedLanguages[0] ?? PLAINTEXT_LANGUAGE_ID; + + if (notebookDocument && textModel) { + const index = notebookDocument.length - 1; + const value = textModel.getValue(); + + if (isFalsyOrWhitespace(value)) { + return; + } + + historyService.addToHistory(notebookDocument.uri, value); + textModel.setValue(''); + notebookDocument.cells[index].resetTextBuffer(textModel.getTextBuffer()); + + const collapseState = editorControl.notebookEditor.notebookOptions.getDisplayOptions().interactiveWindowCollapseCodeCells === 'fromEditor' ? + { + inputCollapsed: false, + outputCollapsed: false + } : + undefined; + + await bulkEditService.apply([ + new ResourceNotebookCellEdit(notebookDocument.uri, + { + editType: CellEditType.Replace, + index: index, + count: 0, + cells: [{ + cellKind: CellKind.Code, + mime: undefined, + language, + source: value, + outputs: [], + metadata: {}, + collapseState + }] + } + ) + ]); + + // reveal the cell into view first + const range = { start: index, end: index + 1 }; + editorControl.notebookEditor.revealCellRangeInView(range); + await editorControl.notebookEditor.executeNotebookCells(editorControl.notebookEditor.getCellsInRange({ start: index, end: index + 1 })); + + // update the selection and focus in the extension host model + const editor = notebookEditorService.getNotebookEditor(editorControl.notebookEditor.getId()); + if (editor) { + editor.setSelections([range]); + editor.setFocus(range); + } + } + } +} diff --git a/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts b/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts new file mode 100644 index 00000000000..67a803eab40 --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts @@ -0,0 +1,725 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/interactive'; +import * as nls from 'vs/nls'; +import * as DOM from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { ICodeEditorViewState, IDecorationOptions } from 'vs/editor/common/editorCommon'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { EditorPaneSelectionChangeReason, IEditorMemento, IEditorOpenContext, IEditorPaneScrollPosition, IEditorPaneSelectionChangeEvent, IEditorPaneWithScrolling } from 'vs/workbench/common/editor'; +import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; +import { ICellViewModel, INotebookEditorOptions, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; +import { IBorrowValue, INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ExecutionStateCellStatusBarContrib, TimerCellStatusBarContrib } from 'vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController'; +import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { InteractiveWindowSetting, INTERACTIVE_INPUT_CURSOR_BOUNDARY } from 'vs/workbench/contrib/interactive/browser/interactiveCommon'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IAction } from 'vs/base/common/actions'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { ParameterHintsController } from 'vs/editor/contrib/parameterHints/browser/parameterHints'; +import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; +import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; +import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion'; +import { MarkerController } from 'vs/editor/contrib/gotoError/browser/gotoError'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; +import { ITextEditorOptions, TextEditorSelectionSource } from 'vs/platform/editor/common/editor'; +import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { NOTEBOOK_KERNEL } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { isEqual } from 'vs/base/common/resources'; +import { NotebookFindContrib } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; +import { EXECUTE_REPL_COMMAND_ID, REPL_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import 'vs/css!./interactiveEditor'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { deepClone } from 'vs/base/common/objects'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ReplEditorInput } from 'vs/workbench/contrib/replNotebook/browser/replEditorInput'; + +const DECORATION_KEY = 'interactiveInputDecoration'; +const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; + +const INPUT_CELL_VERTICAL_PADDING = 8; +const INPUT_CELL_HORIZONTAL_PADDING_RIGHT = 10; +const INPUT_EDITOR_PADDING = 8; + +export interface InteractiveEditorViewState { + readonly notebook?: INotebookEditorViewState; + readonly input?: ICodeEditorViewState | null; +} + +export interface InteractiveEditorOptions extends ITextEditorOptions { + readonly viewState?: InteractiveEditorViewState; +} + +export class ReplEditor extends EditorPane implements IEditorPaneWithScrolling { + private _rootElement!: HTMLElement; + private _styleElement!: HTMLStyleElement; + private _notebookEditorContainer!: HTMLElement; + private _notebookWidget: IBorrowValue = { value: undefined }; + private _inputCellContainer!: HTMLElement; + private _inputFocusIndicator!: HTMLElement; + private _inputRunButtonContainer!: HTMLElement; + private _inputEditorContainer!: HTMLElement; + private _codeEditorWidget!: CodeEditorWidget; + private _notebookWidgetService: INotebookEditorService; + private _instantiationService: IInstantiationService; + private _languageService: ILanguageService; + private _contextKeyService: IContextKeyService; + private _configurationService: IConfigurationService; + private _notebookKernelService: INotebookKernelService; + private _keybindingService: IKeybindingService; + private _menuService: IMenuService; + private _contextMenuService: IContextMenuService; + private _editorGroupService: IEditorGroupsService; + private _extensionService: IExtensionService; + private readonly _widgetDisposableStore: DisposableStore = this._register(new DisposableStore()); + private _lastLayoutDimensions?: { readonly dimension: DOM.Dimension; readonly position: DOM.IDomPosition }; + private _editorOptions: IEditorOptions; + private _notebookOptions: NotebookOptions; + private _editorMemento: IEditorMemento; + private readonly _groupListener = this._register(new MutableDisposable()); + private _runbuttonToolbar: ToolBar | undefined; + + private _onDidFocusWidget = this._register(new Emitter()); + override get onDidFocus(): Event { return this._onDidFocusWidget.event; } + private _onDidChangeSelection = this._register(new Emitter()); + readonly onDidChangeSelection = this._onDidChangeSelection.event; + private _onDidChangeScroll = this._register(new Emitter()); + readonly onDidChangeScroll = this._onDidChangeScroll.event; + + constructor( + group: IEditorGroup, + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IInstantiationService instantiationService: IInstantiationService, + @INotebookEditorService notebookWidgetService: INotebookEditorService, + @IContextKeyService contextKeyService: IContextKeyService, + @ICodeEditorService codeEditorService: ICodeEditorService, + @INotebookKernelService notebookKernelService: INotebookKernelService, + @ILanguageService languageService: ILanguageService, + @IKeybindingService keybindingService: IKeybindingService, + @IConfigurationService configurationService: IConfigurationService, + @IMenuService menuService: IMenuService, + @IContextMenuService contextMenuService: IContextMenuService, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, + @IExtensionService extensionService: IExtensionService, + ) { + super( + REPL_EDITOR_ID, + group, + telemetryService, + themeService, + storageService + ); + this._instantiationService = instantiationService; + this._notebookWidgetService = notebookWidgetService; + this._contextKeyService = contextKeyService; + this._configurationService = configurationService; + this._notebookKernelService = notebookKernelService; + this._languageService = languageService; + this._keybindingService = keybindingService; + this._menuService = menuService; + this._contextMenuService = contextMenuService; + this._editorGroupService = editorGroupService; + this._extensionService = extensionService; + + this._editorOptions = this._computeEditorOptions(); + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor') || e.affectsConfiguration('notebook')) { + this._editorOptions = this._computeEditorOptions(); + } + })); + this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); + this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); + + codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); + this._register(this._keybindingService.onDidUpdateKeybindings(this._updateInputDecoration, this)); + this._register(notebookExecutionStateService.onDidChangeExecution((e) => { + if (e.type === NotebookExecutionType.cell && isEqual(e.notebook, this._notebookWidget.value?.viewModel?.notebookDocument.uri)) { + const cell = this._notebookWidget.value?.getCellByHandle(e.cellHandle); + if (cell && e.changed?.state) { + this._scrollIfNecessary(cell); + } + } + })); + } + + private get inputCellContainerHeight() { + return 19 + 2 + INPUT_CELL_VERTICAL_PADDING * 2 + INPUT_EDITOR_PADDING * 2; + } + + private get inputCellEditorHeight() { + return 19 + INPUT_EDITOR_PADDING * 2; + } + + protected createEditor(parent: HTMLElement): void { + this._rootElement = DOM.append(parent, DOM.$('.interactive-editor')); + this._rootElement.style.position = 'relative'; + this._notebookEditorContainer = DOM.append(this._rootElement, DOM.$('.notebook-editor-container')); + this._inputCellContainer = DOM.append(this._rootElement, DOM.$('.input-cell-container')); + this._inputCellContainer.style.position = 'absolute'; + this._inputCellContainer.style.height = `${this.inputCellContainerHeight}px`; + this._inputFocusIndicator = DOM.append(this._inputCellContainer, DOM.$('.input-focus-indicator')); + this._inputRunButtonContainer = DOM.append(this._inputCellContainer, DOM.$('.run-button-container')); + this._setupRunButtonToolbar(this._inputRunButtonContainer); + this._inputEditorContainer = DOM.append(this._inputCellContainer, DOM.$('.input-editor-container')); + this._createLayoutStyles(); + } + + private _setupRunButtonToolbar(runButtonContainer: HTMLElement) { + const menu = this._register(this._menuService.createMenu(MenuId.ReplInputExecute, this._contextKeyService)); + this._runbuttonToolbar = this._register(new ToolBar(runButtonContainer, this._contextMenuService, { + getKeyBinding: action => this._keybindingService.lookupKeybinding(action.id), + actionViewItemProvider: (action, options) => { + return createActionViewItem(this._instantiationService, action, options); + }, + renderDropdownAsChildElement: true + })); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, result); + this._runbuttonToolbar.setActions([...primary, ...secondary]); + } + + private _createLayoutStyles(): void { + this._styleElement = DOM.createStyleSheet(this._rootElement); + const styleSheets: string[] = []; + + const { + codeCellLeftMargin, + cellRunGutter + } = this._notebookOptions.getLayoutConfiguration(); + const { + focusIndicator + } = this._notebookOptions.getDisplayOptions(); + const leftMargin = this._notebookOptions.getCellEditorContainerLeftMargin(); + + styleSheets.push(` + .interactive-editor .input-cell-container { + padding: ${INPUT_CELL_VERTICAL_PADDING}px ${INPUT_CELL_HORIZONTAL_PADDING_RIGHT}px ${INPUT_CELL_VERTICAL_PADDING}px ${leftMargin}px; + } + `); + if (focusIndicator === 'gutter') { + styleSheets.push(` + .interactive-editor .input-cell-container:focus-within .input-focus-indicator::before { + border-color: var(--vscode-notebook-focusedCellBorder) !important; + } + .interactive-editor .input-focus-indicator::before { + border-color: var(--vscode-notebook-inactiveFocusedCellBorder) !important; + } + .interactive-editor .input-cell-container .input-focus-indicator { + display: block; + top: ${INPUT_CELL_VERTICAL_PADDING}px; + } + .interactive-editor .input-cell-container { + border-top: 1px solid var(--vscode-notebook-inactiveFocusedCellBorder); + } + `); + } else { + // border + styleSheets.push(` + .interactive-editor .input-cell-container { + border-top: 1px solid var(--vscode-notebook-inactiveFocusedCellBorder); + } + .interactive-editor .input-cell-container .input-focus-indicator { + display: none; + } + `); + } + + styleSheets.push(` + .interactive-editor .input-cell-container .run-button-container { + width: ${cellRunGutter}px; + left: ${codeCellLeftMargin}px; + margin-top: ${INPUT_EDITOR_PADDING - 2}px; + } + `); + + this._styleElement.textContent = styleSheets.join('\n'); + } + + private _computeEditorOptions(): IEditorOptions { + let overrideIdentifier: string | undefined = undefined; + if (this._codeEditorWidget) { + overrideIdentifier = this._codeEditorWidget.getModel()?.getLanguageId(); + } + const editorOptions = deepClone(this._configurationService.getValue('editor', { overrideIdentifier })); + const editorOptionsOverride = getSimpleEditorOptions(this._configurationService); + const computed = Object.freeze({ + ...editorOptions, + ...editorOptionsOverride, + ...{ + glyphMargin: true, + padding: { + top: INPUT_EDITOR_PADDING, + bottom: INPUT_EDITOR_PADDING + }, + hover: { + enabled: true + } + } + }); + + return computed; + } + + protected override saveState(): void { + this._saveEditorViewState(this.input); + super.saveState(); + } + + override getViewState(): InteractiveEditorViewState | undefined { + const input = this.input; + if (!(input instanceof ReplEditorInput)) { + return undefined; + } + + this._saveEditorViewState(input); + return this._loadNotebookEditorViewState(input); + } + + private _saveEditorViewState(input: EditorInput | undefined): void { + if (this._notebookWidget.value && input instanceof ReplEditorInput) { + if (this._notebookWidget.value.isDisposed) { + return; + } + + const state = this._notebookWidget.value.getEditorViewState(); + const editorState = this._codeEditorWidget.saveViewState(); + this._editorMemento.saveEditorState(this.group, input.resource, { + notebook: state, + input: editorState + }); + } + } + + private _loadNotebookEditorViewState(input: ReplEditorInput): InteractiveEditorViewState | undefined { + const result = this._editorMemento.loadEditorState(this.group, input.resource); + if (result) { + return result; + } + // when we don't have a view state for the group/input-tuple then we try to use an existing + // editor for the same resource. + for (const group of this._editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { + if (group.activeEditorPane !== this && group.activeEditorPane === this && group.activeEditor?.matches(input)) { + const notebook = this._notebookWidget.value?.getEditorViewState(); + const input = this._codeEditorWidget.saveViewState(); + return { + notebook, + input + }; + } + } + return; + } + + override async setInput(input: ReplEditorInput, options: InteractiveEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + // there currently is a widget which we still own so + // we need to hide it before getting a new widget + this._notebookWidget.value?.onWillHide(); + + this._codeEditorWidget?.dispose(); + + this._widgetDisposableStore.clear(); + + this._notebookWidget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, this.group, input, { + isEmbedded: true, + isReadOnly: true, + forRepl: true, + contributions: NotebookEditorExtensionsRegistry.getSomeEditorContributions([ + ExecutionStateCellStatusBarContrib.id, + TimerCellStatusBarContrib.id, + NotebookFindContrib.id + ]), + menuIds: { + notebookToolbar: MenuId.InteractiveToolbar, + cellTitleToolbar: MenuId.InteractiveCellTitle, + cellDeleteToolbar: MenuId.InteractiveCellDelete, + cellInsertToolbar: MenuId.NotebookCellBetween, + cellTopInsertToolbar: MenuId.NotebookCellListTop, + cellExecuteToolbar: MenuId.InteractiveCellExecute, + cellExecutePrimary: undefined + }, + cellEditorContributions: EditorExtensionsRegistry.getSomeEditorContributions([ + SelectionClipboardContributionID, + ContextMenuController.ID, + HoverController.ID, + MarkerController.ID + ]), + options: this._notebookOptions, + codeWindow: this.window + }, undefined, this.window); + + this._codeEditorWidget = this._instantiationService.createInstance(CodeEditorWidget, this._inputEditorContainer, this._editorOptions, { + ...{ + isSimpleWidget: false, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + MenuPreventer.ID, + SelectionClipboardContributionID, + ContextMenuController.ID, + SuggestController.ID, + ParameterHintsController.ID, + SnippetController2.ID, + TabCompletionController.ID, + HoverController.ID, + MarkerController.ID + ]) + } + }); + + if (this._lastLayoutDimensions) { + this._notebookEditorContainer.style.height = `${this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight}px`; + this._notebookWidget.value!.layout(new DOM.Dimension(this._lastLayoutDimensions.dimension.width, this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight), this._notebookEditorContainer); + const leftMargin = this._notebookOptions.getCellEditorContainerLeftMargin(); + const maxHeight = Math.min(this._lastLayoutDimensions.dimension.height / 2, this.inputCellEditorHeight); + this._codeEditorWidget.layout(this._validateDimension(this._lastLayoutDimensions.dimension.width - leftMargin - INPUT_CELL_HORIZONTAL_PADDING_RIGHT, maxHeight)); + this._inputFocusIndicator.style.height = `${this.inputCellEditorHeight}px`; + this._inputCellContainer.style.top = `${this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight}px`; + this._inputCellContainer.style.width = `${this._lastLayoutDimensions.dimension.width}px`; + } + + await super.setInput(input, options, context, token); + const model = await input.resolve(); + if (this._runbuttonToolbar) { + this._runbuttonToolbar.context = input.resource; + } + + if (model === null) { + throw new Error('The REPL model could not be resolved'); + } + + this._notebookWidget.value?.setParentContextKeyService(this._contextKeyService); + + const viewState = options?.viewState ?? this._loadNotebookEditorViewState(input); + await this._extensionService.whenInstalledExtensionsRegistered(); + await this._notebookWidget.value!.setModel(model.notebook, viewState?.notebook); + model.notebook.setCellCollapseDefault(this._notebookOptions.getCellCollapseDefault()); + this._notebookWidget.value!.setOptions({ + isReadOnly: true + }); + this._widgetDisposableStore.add(this._notebookWidget.value!.onDidResizeOutput((cvm) => { + this._scrollIfNecessary(cvm); + })); + this._widgetDisposableStore.add(this._notebookWidget.value!.onDidFocusWidget(() => this._onDidFocusWidget.fire())); + this._widgetDisposableStore.add(this._notebookOptions.onDidChangeOptions(e => { + if (e.compactView || e.focusIndicator) { + // update the styling + this._styleElement?.remove(); + this._createLayoutStyles(); + } + + if (this._lastLayoutDimensions && this.isVisible()) { + this.layout(this._lastLayoutDimensions.dimension, this._lastLayoutDimensions.position); + } + + if (e.interactiveWindowCollapseCodeCells) { + model.notebook.setCellCollapseDefault(this._notebookOptions.getCellCollapseDefault()); + } + })); + + const editorModel = await input.resolveInput(model.notebook); + this._codeEditorWidget.setModel(editorModel); + if (viewState?.input) { + this._codeEditorWidget.restoreViewState(viewState.input); + } + this._editorOptions = this._computeEditorOptions(); + this._codeEditorWidget.updateOptions(this._editorOptions); + + this._widgetDisposableStore.add(this._codeEditorWidget.onDidFocusEditorWidget(() => this._onDidFocusWidget.fire())); + this._widgetDisposableStore.add(this._codeEditorWidget.onDidContentSizeChange(e => { + if (!e.contentHeightChanged) { + return; + } + + if (this._lastLayoutDimensions) { + this._layoutWidgets(this._lastLayoutDimensions.dimension, this._lastLayoutDimensions.position); + } + })); + + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeCursorPosition(e => this._onDidChangeSelection.fire({ reason: this._toEditorPaneSelectionChangeReason(e) }))); + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeModelContent(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.EDIT }))); + + + this._widgetDisposableStore.add(this._notebookKernelService.onDidChangeNotebookAffinity(this._syncWithKernel, this)); + this._widgetDisposableStore.add(this._notebookKernelService.onDidChangeSelectedNotebooks(this._syncWithKernel, this)); + + this._widgetDisposableStore.add(this.themeService.onDidColorThemeChange(() => { + if (this.isVisible()) { + this._updateInputDecoration(); + } + })); + + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeModelContent(() => { + if (this.isVisible()) { + this._updateInputDecoration(); + } + })); + + const cursorAtBoundaryContext = INTERACTIVE_INPUT_CURSOR_BOUNDARY.bindTo(this._contextKeyService); + if (input.resource && input.historyService.has(input.resource)) { + cursorAtBoundaryContext.set('top'); + } else { + cursorAtBoundaryContext.set('none'); + } + + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeCursorPosition(({ position }) => { + const viewModel = this._codeEditorWidget._getViewModel()!; + const lastLineNumber = viewModel.getLineCount(); + const lastLineCol = viewModel.getLineLength(lastLineNumber) + 1; + const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(position); + const firstLine = viewPosition.lineNumber === 1 && viewPosition.column === 1; + const lastLine = viewPosition.lineNumber === lastLineNumber && viewPosition.column === lastLineCol; + + if (firstLine) { + if (lastLine) { + cursorAtBoundaryContext.set('both'); + } else { + cursorAtBoundaryContext.set('top'); + } + } else { + if (lastLine) { + cursorAtBoundaryContext.set('bottom'); + } else { + cursorAtBoundaryContext.set('none'); + } + } + })); + + this._widgetDisposableStore.add(editorModel.onDidChangeContent(() => { + const value = editorModel.getValue(); + if (this.input?.resource && value !== '') { + (this.input as ReplEditorInput).historyService.replaceLast(this.input.resource, value); + } + })); + + this._widgetDisposableStore.add(this._notebookWidget.value!.onDidScroll(() => this._onDidChangeScroll.fire())); + + this._syncWithKernel(); + } + + override setOptions(options: INotebookEditorOptions | undefined): void { + this._notebookWidget.value?.setOptions(options); + super.setOptions(options); + } + + private _toEditorPaneSelectionChangeReason(e: ICursorPositionChangedEvent): EditorPaneSelectionChangeReason { + switch (e.source) { + case TextEditorSelectionSource.PROGRAMMATIC: return EditorPaneSelectionChangeReason.PROGRAMMATIC; + case TextEditorSelectionSource.NAVIGATION: return EditorPaneSelectionChangeReason.NAVIGATION; + case TextEditorSelectionSource.JUMP: return EditorPaneSelectionChangeReason.JUMP; + default: return EditorPaneSelectionChangeReason.USER; + } + } + + private _cellAtBottom(cell: ICellViewModel): boolean { + const visibleRanges = this._notebookWidget.value?.visibleRanges || []; + const cellIndex = this._notebookWidget.value?.getCellIndex(cell); + if (cellIndex === Math.max(...visibleRanges.map(range => range.end - 1))) { + return true; + } + return false; + } + + private _scrollIfNecessary(cvm: ICellViewModel) { + const index = this._notebookWidget.value!.getCellIndex(cvm); + if (index === this._notebookWidget.value!.getLength() - 1) { + // If we're already at the bottom or auto scroll is enabled, scroll to the bottom + if (this._configurationService.getValue(InteractiveWindowSetting.interactiveWindowAlwaysScrollOnNewCell) || this._cellAtBottom(cvm)) { + this._notebookWidget.value!.scrollToBottom(); + } + } + } + + private _syncWithKernel() { + const notebook = this._notebookWidget.value?.textModel; + const textModel = this._codeEditorWidget.getModel(); + + if (notebook && textModel) { + const info = this._notebookKernelService.getMatchingKernel(notebook); + const selectedOrSuggested = info.selected + ?? (info.suggestions.length === 1 ? info.suggestions[0] : undefined) + ?? (info.all.length === 1 ? info.all[0] : undefined); + + if (selectedOrSuggested) { + const language = selectedOrSuggested.supportedLanguages[0]; + // All kernels will initially list plaintext as the supported language before they properly initialized. + if (language && language !== 'plaintext') { + const newMode = this._languageService.createById(language).languageId; + textModel.setLanguage(newMode); + } + + NOTEBOOK_KERNEL.bindTo(this._contextKeyService).set(selectedOrSuggested.id); + } + } + + this._updateInputDecoration(); + } + + layout(dimension: DOM.Dimension, position: DOM.IDomPosition): void { + this._rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); + this._rootElement.classList.toggle('narrow-width', dimension.width < 600); + const editorHeightChanged = dimension.height !== this._lastLayoutDimensions?.dimension.height; + this._lastLayoutDimensions = { dimension, position }; + + if (!this._notebookWidget.value) { + return; + } + + if (editorHeightChanged && this._codeEditorWidget) { + SuggestController.get(this._codeEditorWidget)?.cancelSuggestWidget(); + } + + this._notebookEditorContainer.style.height = `${this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight}px`; + this._layoutWidgets(dimension, position); + } + + private _layoutWidgets(dimension: DOM.Dimension, position: DOM.IDomPosition) { + const contentHeight = this._codeEditorWidget.hasModel() ? this._codeEditorWidget.getContentHeight() : this.inputCellEditorHeight; + const maxHeight = Math.min(dimension.height / 2, contentHeight); + const leftMargin = this._notebookOptions.getCellEditorContainerLeftMargin(); + + const inputCellContainerHeight = maxHeight + INPUT_CELL_VERTICAL_PADDING * 2; + this._notebookEditorContainer.style.height = `${dimension.height - inputCellContainerHeight}px`; + + this._notebookWidget.value!.layout(dimension.with(dimension.width, dimension.height - inputCellContainerHeight), this._notebookEditorContainer, position); + this._codeEditorWidget.layout(this._validateDimension(dimension.width - leftMargin - INPUT_CELL_HORIZONTAL_PADDING_RIGHT, maxHeight)); + this._inputFocusIndicator.style.height = `${contentHeight}px`; + this._inputCellContainer.style.top = `${dimension.height - inputCellContainerHeight}px`; + this._inputCellContainer.style.width = `${dimension.width}px`; + } + + private _validateDimension(width: number, height: number) { + return new DOM.Dimension(Math.max(0, width), Math.max(0, height)); + } + + private _updateInputDecoration(): void { + if (!this._codeEditorWidget) { + return; + } + + if (!this._codeEditorWidget.hasModel()) { + return; + } + + const model = this._codeEditorWidget.getModel(); + + const decorations: IDecorationOptions[] = []; + + if (model?.getValueLength() === 0) { + const transparentForeground = resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4); + const languageId = model.getLanguageId(); + if (languageId !== 'plaintext') { + const keybinding = this._keybindingService.lookupKeybinding(EXECUTE_REPL_COMMAND_ID, this._contextKeyService)?.getLabel(); + const text = keybinding ? + nls.localize('interactiveInputPlaceHolder', "Type '{0}' code here and press {1} to run", languageId, keybinding) : + nls.localize('interactiveInputPlaceHolderNoKeybinding', "Type '{0}' code here and click run", languageId); + decorations.push({ + range: { + startLineNumber: 0, + endLineNumber: 0, + startColumn: 0, + endColumn: 1 + }, + renderOptions: { + after: { + contentText: text, + color: transparentForeground ? transparentForeground.toString() : undefined + } + } + }); + } + + } + + this._codeEditorWidget.setDecorationsByType('interactive-decoration', DECORATION_KEY, decorations); + } + + getScrollPosition(): IEditorPaneScrollPosition { + return { + scrollTop: this._notebookWidget.value?.scrollTop ?? 0, + scrollLeft: 0 + }; + } + + setScrollPosition(position: IEditorPaneScrollPosition): void { + this._notebookWidget.value?.setScrollTop(position.scrollTop); + } + + override focus() { + super.focus(); + + this._notebookWidget.value?.onShow(); + this._codeEditorWidget.focus(); + } + + focusHistory() { + this._notebookWidget.value!.focus(); + } + + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + this._groupListener.value = this.group.onWillCloseEditor(e => this._saveEditorViewState(e.editor)); + + if (!visible) { + this._saveEditorViewState(this.input); + if (this.input && this._notebookWidget.value) { + this._notebookWidget.value.onWillHide(); + } + } + } + + override clearInput() { + if (this._notebookWidget.value) { + this._saveEditorViewState(this.input); + this._notebookWidget.value.onWillHide(); + } + + this._codeEditorWidget?.dispose(); + + this._notebookWidget = { value: undefined }; + this._widgetDisposableStore.clear(); + + super.clearInput(); + } + + override getControl(): { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } { + return { + notebookEditor: this._notebookWidget.value, + codeEditor: this._codeEditorWidget + }; + } +} diff --git a/src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts b/src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts new file mode 100644 index 00000000000..bf82d590a26 --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { IInteractiveHistoryService } from 'vs/workbench/contrib/interactive/browser/interactiveHistoryService'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ICustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; + +export class ReplEditorInput extends NotebookEditorInput { + static override ID: string = 'workbench.editorinputs.replEditorInput'; + + private inputModelRef: IReference | undefined; + private isScratchpad: boolean; + private isDisposing = false; + + constructor( + resource: URI, + @INotebookService _notebookService: INotebookService, + @INotebookEditorModelResolverService _notebookModelResolverService: INotebookEditorModelResolverService, + @IFileDialogService _fileDialogService: IFileDialogService, + @ILabelService labelService: ILabelService, + @IFileService fileService: IFileService, + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IExtensionService extensionService: IExtensionService, + @IEditorService editorService: IEditorService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService, + @IInteractiveHistoryService public readonly historyService: IInteractiveHistoryService, + @ITextModelService private readonly _textModelService: ITextModelService, + @IConfigurationService configurationService: IConfigurationService + ) { + super(resource, undefined, 'jupyter-notebook', {}, _notebookService, _notebookModelResolverService, _fileDialogService, labelService, fileService, filesConfigurationService, extensionService, editorService, textResourceConfigurationService, customEditorLabelService); + this.isScratchpad = configurationService.getValue(NotebookSetting.InteractiveWindowPromptToSave) !== true; + } + + override get typeId(): string { + return ReplEditorInput.ID; + } + + override getName() { + return 'REPL'; + } + + override get capabilities() { + const capabilities = super.capabilities; + const scratchPad = this.isScratchpad ? EditorInputCapabilities.Scratchpad : 0; + + return capabilities + | EditorInputCapabilities.Readonly + | scratchPad; + } + + async resolveInput(notebook: NotebookTextModel) { + if (this.inputModelRef) { + return this.inputModelRef.object.textEditorModel; + } + + const lastCell = notebook.cells[notebook.cells.length - 1]; + this.inputModelRef = await this._textModelService.createModelReference(lastCell.uri); + return this.inputModelRef.object.textEditorModel; + } + + override dispose() { + if (!this.isDisposing) { + this.isDisposing = true; + this.editorModelReference?.object.revert({ soft: true }); + this.inputModelRef?.dispose(); + super.dispose(); + } + } +} diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 7ee1d516f6d..718c1475233 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { basename } from 'vs/base/common/resources'; -import { IDisposable, dispose, Disposable, DisposableStore, combinedDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { VIEW_PANE_ID, ISCMService, ISCMRepository, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; @@ -21,137 +21,168 @@ import { Iterable } from 'vs/base/common/iterator'; import { ITitleService } from 'vs/workbench/services/title/browser/titleService'; import { IEditorGroupContextKeyProvider, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { getRepositoryResourceCount } from 'vs/workbench/contrib/scm/browser/util'; +import { autorun, autorunWithStore, derived, IObservable, observableFromEvent } from 'vs/base/common/observable'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; +import { derivedObservableWithCache, latestChangedValue, observableFromEventOpts } from 'vs/base/common/observableInternal/utils'; +import { Command } from 'vs/editor/common/languages'; +import { ISCMHistoryItemGroup } from 'vs/workbench/contrib/scm/common/history'; +import { ILogService } from 'vs/platform/log/common/log'; + +export class SCMActiveRepositoryController extends Disposable implements IWorkbenchContribution { + private readonly _countBadgeConfig = observableConfigValue<'all' | 'focused' | 'off'>('scm.countBadge', 'all', this.configurationService); + + private readonly _repositories = observableFromEvent(this, + Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), + () => this.scmService.repositories); + + private readonly _focusedRepository = observableFromEventOpts( + { owner: this, equalsFn: () => false }, + this.scmViewService.onDidFocusRepository, + () => this.scmViewService.focusedRepository); + + private readonly _activeEditor = observableFromEventOpts( + { owner: this, equalsFn: () => false }, + this.editorService.onDidActiveEditorChange, + () => this.editorService.activeEditor); + + private readonly _activeEditorRepository = derivedObservableWithCache(this, (reader, lastValue) => { + const activeResource = EditorResourceAccessor.getOriginalUri(this._activeEditor.read(reader)); + if (!activeResource) { + this.logService.trace('SCMActiveRepositoryController (activeEditorRepository derived): no activeResource'); + return lastValue; + } -function getCount(repository: ISCMRepository): number { - if (typeof repository.provider.count === 'number') { - return repository.provider.count; - } else { - return repository.provider.groups.reduce((r, g) => r + g.resources.length, 0); - } -} + const repository = this.scmService.getRepository(activeResource); + if (!repository) { + this.logService.trace(`SCMActiveRepositoryController (activeEditorRepository derived): no repository for '${activeResource.toString()}'`); + return lastValue; + } -export class SCMStatusController implements IWorkbenchContribution { + return Object.create(repository); + }); - private statusBarDisposable: IDisposable = Disposable.None; - private focusDisposable: IDisposable = Disposable.None; - private focusedRepository: ISCMRepository | undefined = undefined; - private readonly badgeDisposable = new MutableDisposable(); - private readonly disposables = new DisposableStore(); - private repositoryDisposables = new Set(); + /** + * The focused repository takes precedence over the active editor repository when the observable + * values are updated in the same transaction (or during the initial read of the observable value). + */ + private readonly _activeRepository = latestChangedValue(this, [this._activeEditorRepository, this._focusedRepository]); - constructor( - @ISCMService private readonly scmService: ISCMService, - @ISCMViewService private readonly scmViewService: ISCMViewService, - @IStatusbarService private readonly statusbarService: IStatusbarService, - @IActivityService private readonly activityService: IActivityService, - @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService - ) { - this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); - this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + private readonly _countBadgeRepositories = derived(this, reader => { + switch (this._countBadgeConfig.read(reader)) { + case 'all': { + const repositories = this._repositories.read(reader); + return [...Iterable.map(repositories, r => ({ ...r.provider, resourceCount: this._getRepositoryResourceCount(r) }))]; + } + case 'focused': { + const repository = this._activeRepository.read(reader); + return repository ? [{ ...repository.provider, resourceCount: this._getRepositoryResourceCount(repository) }] : []; + } + case 'off': + return []; + default: + throw new Error('Invalid countBadge setting'); + } + }); - const onDidChangeSCMCountBadge = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.countBadge')); - onDidChangeSCMCountBadge(this.renderActivityCount, this, this.disposables); + private readonly _countBadge = derived(this, reader => { + let total = 0; - for (const repository of this.scmService.repositories) { - this.onDidAddRepository(repository); + for (const repository of this._countBadgeRepositories.read(reader)) { + const count = repository.count?.read(reader); + const resourceCount = repository.resourceCount.read(reader); + + total = total + (count ?? resourceCount); } - this.scmViewService.onDidFocusRepository(this.focusRepository, this, this.disposables); - this.focusRepository(this.scmViewService.focusedRepository); + return total; + }); - editorService.onDidActiveEditorChange(() => this.tryFocusRepositoryBasedOnActiveEditor(), this, this.disposables); - this.renderActivityCount(); - } + private _activeRepositoryNameContextKey: IContextKey; + private _activeRepositoryBranchNameContextKey: IContextKey; - private tryFocusRepositoryBasedOnActiveEditor(repositories: Iterable = this.scmService.repositories): boolean { - const resource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor); + constructor( + @IActivityService private readonly activityService: IActivityService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IEditorService private readonly editorService: IEditorService, + @ILogService private readonly logService: ILogService, + @ISCMService private readonly scmService: ISCMService, + @ISCMViewService private readonly scmViewService: ISCMViewService, + @IStatusbarService private readonly statusbarService: IStatusbarService, + @ITitleService private readonly titleService: ITitleService + ) { + super(); - if (!resource) { - return false; - } + this._activeRepositoryNameContextKey = ActiveRepositoryContextKeys.ActiveRepositoryName.bindTo(this.contextKeyService); + this._activeRepositoryBranchNameContextKey = ActiveRepositoryContextKeys.ActiveRepositoryBranchName.bindTo(this.contextKeyService); - let bestRepository: ISCMRepository | null = null; - let bestMatchLength = Number.POSITIVE_INFINITY; + this.titleService.registerVariables([ + { name: 'activeRepositoryName', contextKey: ActiveRepositoryContextKeys.ActiveRepositoryName.key }, + { name: 'activeRepositoryBranchName', contextKey: ActiveRepositoryContextKeys.ActiveRepositoryBranchName.key, } + ]); - for (const repository of repositories) { - const root = repository.provider.rootUri; + this._register(autorun(reader => { + const repository = this._focusedRepository.read(reader); + const commands = repository?.provider.statusBarCommands.read(reader); - if (!root) { - continue; - } + this.logService.trace('SCMActiveRepositoryController (focusedRepository):', repository?.id ?? 'no id'); + this.logService.trace('SCMActiveRepositoryController (focusedRepository):', commands ? commands.map(c => c.title).join(', ') : 'no commands'); + })); - const path = this.uriIdentityService.extUri.relativePath(root, resource); + this._register(autorun(reader => { + const repository = this._activeEditorRepository.read(reader); + const commands = repository?.provider.statusBarCommands.read(reader); - if (path && !/^\.\./.test(path) && path.length < bestMatchLength) { - bestRepository = repository; - bestMatchLength = path.length; - } - } + this.logService.trace('SCMActiveRepositoryController (activeEditorRepository):', repository?.id ?? 'no id'); + this.logService.trace('SCMActiveRepositoryController (activeEditorRepository):', commands ? commands.map(c => c.title).join(', ') : 'no commands'); + })); - if (!bestRepository) { - return false; - } + this._register(autorunWithStore((reader, store) => { + this._updateActivityCountBadge(this._countBadge.read(reader), store); + })); - this.focusRepository(bestRepository); - return true; - } + this._register(autorunWithStore((reader, store) => { + const repository = this._activeRepository.read(reader); + const commands = repository?.provider.statusBarCommands.read(reader); - private onDidAddRepository(repository: ISCMRepository): void { - const onDidChange = Event.any(repository.provider.onDidChange, repository.provider.onDidChangeResources); - const changeDisposable = onDidChange(() => this.renderActivityCount()); + this.logService.trace('SCMActiveRepositoryController (status bar):', repository?.id ?? 'no id'); + this.logService.trace('SCMActiveRepositoryController (status bar):', commands ? commands.map(c => c.title).join(', ') : 'no commands'); - const onDidRemove = Event.filter(this.scmService.onDidRemoveRepository, e => e === repository); - const removeDisposable = onDidRemove(() => { - disposable.dispose(); - this.repositoryDisposables.delete(disposable); - this.renderActivityCount(); - }); + this._updateStatusBar(repository, commands ?? [], store); + })); - const disposable = combinedDisposable(changeDisposable, removeDisposable); - this.repositoryDisposables.add(disposable); + this._register(autorun(reader => { + const repository = this._activeRepository.read(reader); + const currentHistoryItemGroup = repository?.provider.historyProviderObs.read(reader)?.currentHistoryItemGroupObs.read(reader); - this.tryFocusRepositoryBasedOnActiveEditor(Iterable.single(repository)); + this._updateActiveRepositoryContextKeys(repository, currentHistoryItemGroup); + })); } - private onDidRemoveRepository(repository: ISCMRepository): void { - if (this.focusedRepository !== repository) { - return; - } - - this.focusRepository(Iterable.first(this.scmService.repositories)); + private _getRepositoryResourceCount(repository: ISCMRepository): IObservable { + return observableFromEvent(this, repository.provider.onDidChangeResources, () => /** @description repositoryResourceCount */ getRepositoryResourceCount(repository.provider)); } - private focusRepository(repository: ISCMRepository | undefined): void { - if (this.focusedRepository === repository) { + private _updateActivityCountBadge(count: number, store: DisposableStore): void { + if (count === 0) { return; } - this.focusDisposable.dispose(); - this.focusedRepository = repository; - - if (repository && repository.provider.onDidChangeStatusBarCommands) { - this.focusDisposable = repository.provider.onDidChangeStatusBarCommands(() => this.renderStatusBar(repository)); - } - - this.renderStatusBar(repository); - this.renderActivityCount(); + const badge = new NumberBadge(count, num => localize('scmPendingChangesBadge', '{0} pending changes', num)); + store.add(this.activityService.showViewActivity(VIEW_PANE_ID, { badge })); } - private renderStatusBar(repository: ISCMRepository | undefined): void { - this.statusBarDisposable.dispose(); - + private _updateStatusBar(repository: ISCMRepository | undefined, commands: readonly Command[], store: DisposableStore): void { if (!repository) { + this.logService.trace('SCMActiveRepositoryController (status bar): repository is undefined'); return; } - const commands = repository.provider.statusBarCommands || []; const label = repository.provider.rootUri ? `${basename(repository.provider.rootUri)} (${repository.provider.label})` : repository.provider.label; - const disposables = new DisposableStore(); for (let index = 0; index < commands.length; index++) { const command = commands[index]; const tooltip = `${label}${command.tooltip ? ` - ${command.tooltip}` : ''}`; @@ -178,41 +209,16 @@ export class SCMStatusController implements IWorkbenchContribution { command: command.id ? command : undefined }; - disposables.add(index === 0 ? + store.add(index === 0 ? this.statusbarService.addEntry(statusbarEntry, `status.scm.${index}`, MainThreadStatusBarAlignment.LEFT, 10000) : this.statusbarService.addEntry(statusbarEntry, `status.scm.${index}`, MainThreadStatusBarAlignment.LEFT, { id: `status.scm.${index - 1}`, alignment: MainThreadStatusBarAlignment.RIGHT, compact: true }) ); } - - this.statusBarDisposable = disposables; } - private renderActivityCount(): void { - const countBadgeType = this.configurationService.getValue<'all' | 'focused' | 'off'>('scm.countBadge'); - - let count = 0; - - if (countBadgeType === 'all') { - count = Iterable.reduce(this.scmService.repositories, (r, repository) => r + getCount(repository), 0); - } else if (countBadgeType === 'focused' && this.focusedRepository) { - count = getCount(this.focusedRepository); - } - - if (count > 0) { - const badge = new NumberBadge(count, num => localize('scmPendingChangesBadge', '{0} pending changes', num)); - this.badgeDisposable.value = this.activityService.showViewActivity(VIEW_PANE_ID, { badge }); - } else { - this.badgeDisposable.value = undefined; - } - } - - dispose(): void { - this.focusDisposable.dispose(); - this.statusBarDisposable.dispose(); - this.badgeDisposable.dispose(); - this.disposables.dispose(); - dispose(this.repositoryDisposables.values()); - this.repositoryDisposables.clear(); + private _updateActiveRepositoryContextKeys(repository: ISCMRepository | undefined, currentHistoryItemGroup: ISCMHistoryItemGroup | undefined): void { + this._activeRepositoryNameContextKey.set(repository?.provider.name ?? ''); + this._activeRepositoryBranchNameContextKey.set(currentHistoryItemGroup?.name ?? ''); } } @@ -221,76 +227,6 @@ const ActiveRepositoryContextKeys = { ActiveRepositoryBranchName: new RawContextKey('scmActiveRepositoryBranchName', ''), }; -export class SCMActiveRepositoryContextKeyController implements IWorkbenchContribution { - - private activeRepositoryNameContextKey: IContextKey; - private activeRepositoryBranchNameContextKey: IContextKey; - - private focusedRepository: ISCMRepository | undefined = undefined; - private focusDisposable: IDisposable = Disposable.None; - private readonly disposables = new DisposableStore(); - - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @IEditorService private readonly editorService: IEditorService, - @ISCMViewService private readonly scmViewService: ISCMViewService, - @ITitleService titleService: ITitleService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService - ) { - this.activeRepositoryNameContextKey = ActiveRepositoryContextKeys.ActiveRepositoryName.bindTo(contextKeyService); - this.activeRepositoryBranchNameContextKey = ActiveRepositoryContextKeys.ActiveRepositoryBranchName.bindTo(contextKeyService); - - titleService.registerVariables([ - { name: 'activeRepositoryName', contextKey: ActiveRepositoryContextKeys.ActiveRepositoryName.key }, - { name: 'activeRepositoryBranchName', contextKey: ActiveRepositoryContextKeys.ActiveRepositoryBranchName.key, } - ]); - - editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.disposables); - scmViewService.onDidFocusRepository(this.onDidFocusRepository, this, this.disposables); - this.onDidFocusRepository(scmViewService.focusedRepository); - } - - private onDidActiveEditorChange(): void { - const activeResource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor); - - if (activeResource?.scheme !== Schemas.file && activeResource?.scheme !== Schemas.vscodeRemote) { - return; - } - - const repository = Iterable.find( - this.scmViewService.repositories, - r => Boolean(r.provider.rootUri && this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri)) - ); - - this.onDidFocusRepository(repository); - } - - private onDidFocusRepository(repository: ISCMRepository | undefined): void { - if (!repository || this.focusedRepository === repository) { - return; - } - - this.focusDisposable.dispose(); - this.focusedRepository = repository; - - if (repository && repository.provider.onDidChangeStatusBarCommands) { - this.focusDisposable = repository.provider.onDidChangeStatusBarCommands(() => this.updateContextKeys(repository)); - } - - this.updateContextKeys(repository); - } - - private updateContextKeys(repository: ISCMRepository | undefined): void { - this.activeRepositoryNameContextKey.set(repository?.provider.name ?? ''); - this.activeRepositoryBranchNameContextKey.set(repository?.provider.historyProvider?.currentHistoryItemGroup?.name ?? ''); - } - - dispose(): void { - this.focusDisposable.dispose(); - this.disposables.dispose(); - } -} - export class SCMActiveResourceContextKeyController implements IWorkbenchContribution { private readonly disposables = new DisposableStore(); diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 7537f80f534..173cdb04fd0 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -1013,37 +1013,17 @@ const editorGutterAddedBackground = registerColor('editorGutter.addedBackground' hcLight: '#48985D' }, nls.localize('editorGutterAddedBackground', "Editor gutter background color for lines that are added.")); -const editorGutterDeletedBackground = registerColor('editorGutter.deletedBackground', { - dark: editorErrorForeground, - light: editorErrorForeground, - hcDark: editorErrorForeground, - hcLight: editorErrorForeground -}, nls.localize('editorGutterDeletedBackground', "Editor gutter background color for lines that are deleted.")); - -const minimapGutterModifiedBackground = registerColor('minimapGutter.modifiedBackground', { - dark: editorGutterModifiedBackground, - light: editorGutterModifiedBackground, - hcDark: editorGutterModifiedBackground, - hcLight: editorGutterModifiedBackground -}, nls.localize('minimapGutterModifiedBackground', "Minimap gutter background color for lines that are modified.")); - -const minimapGutterAddedBackground = registerColor('minimapGutter.addedBackground', { - dark: editorGutterAddedBackground, - light: editorGutterAddedBackground, - hcDark: editorGutterAddedBackground, - hcLight: editorGutterAddedBackground -}, nls.localize('minimapGutterAddedBackground', "Minimap gutter background color for lines that are added.")); - -const minimapGutterDeletedBackground = registerColor('minimapGutter.deletedBackground', { - dark: editorGutterDeletedBackground, - light: editorGutterDeletedBackground, - hcDark: editorGutterDeletedBackground, - hcLight: editorGutterDeletedBackground -}, nls.localize('minimapGutterDeletedBackground', "Minimap gutter background color for lines that are deleted.")); - -const overviewRulerModifiedForeground = registerColor('editorOverviewRuler.modifiedForeground', { dark: transparent(editorGutterModifiedBackground, 0.6), light: transparent(editorGutterModifiedBackground, 0.6), hcDark: transparent(editorGutterModifiedBackground, 0.6), hcLight: transparent(editorGutterModifiedBackground, 0.6) }, nls.localize('overviewRulerModifiedForeground', 'Overview ruler marker color for modified content.')); -const overviewRulerAddedForeground = registerColor('editorOverviewRuler.addedForeground', { dark: transparent(editorGutterAddedBackground, 0.6), light: transparent(editorGutterAddedBackground, 0.6), hcDark: transparent(editorGutterAddedBackground, 0.6), hcLight: transparent(editorGutterAddedBackground, 0.6) }, nls.localize('overviewRulerAddedForeground', 'Overview ruler marker color for added content.')); -const overviewRulerDeletedForeground = registerColor('editorOverviewRuler.deletedForeground', { dark: transparent(editorGutterDeletedBackground, 0.6), light: transparent(editorGutterDeletedBackground, 0.6), hcDark: transparent(editorGutterDeletedBackground, 0.6), hcLight: transparent(editorGutterDeletedBackground, 0.6) }, nls.localize('overviewRulerDeletedForeground', 'Overview ruler marker color for deleted content.')); +const editorGutterDeletedBackground = registerColor('editorGutter.deletedBackground', editorErrorForeground, nls.localize('editorGutterDeletedBackground', "Editor gutter background color for lines that are deleted.")); + +const minimapGutterModifiedBackground = registerColor('minimapGutter.modifiedBackground', editorGutterModifiedBackground, nls.localize('minimapGutterModifiedBackground', "Minimap gutter background color for lines that are modified.")); + +const minimapGutterAddedBackground = registerColor('minimapGutter.addedBackground', editorGutterAddedBackground, nls.localize('minimapGutterAddedBackground', "Minimap gutter background color for lines that are added.")); + +const minimapGutterDeletedBackground = registerColor('minimapGutter.deletedBackground', editorGutterDeletedBackground, nls.localize('minimapGutterDeletedBackground', "Minimap gutter background color for lines that are deleted.")); + +const overviewRulerModifiedForeground = registerColor('editorOverviewRuler.modifiedForeground', transparent(editorGutterModifiedBackground, 0.6), nls.localize('overviewRulerModifiedForeground', 'Overview ruler marker color for modified content.')); +const overviewRulerAddedForeground = registerColor('editorOverviewRuler.addedForeground', transparent(editorGutterAddedBackground, 0.6), nls.localize('overviewRulerAddedForeground', 'Overview ruler marker color for added content.')); +const overviewRulerDeletedForeground = registerColor('editorOverviewRuler.deletedForeground', transparent(editorGutterDeletedBackground, 0.6), nls.localize('overviewRulerDeletedForeground', 'Overview ruler marker color for deleted content.')); class DirtyDiffDecorator extends Disposable { diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index c8939c63fbe..7b94eac4187 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -67,10 +67,6 @@ justify-content: flex-end; } -.scm-view .monaco-editor .selected-text { - border-radius: 0; -} - /** * The following rules are very specific because of inline drop down menus * https://github.com/microsoft/vscode/issues/101410 @@ -133,6 +129,31 @@ align-items: center; } +.scm-view .monaco-list-row .history-item > .graph-container { + display: flex; + flex-shrink: 0; + height: 22px; +} + +.scm-view .monaco-list-row .history-item > .graph-container > .graph > circle { + stroke: var(--vscode-sideBar-background); +} + +.scm-view .monaco-list-row .history-item > .label-container { + display: flex; + opacity: 0.75; + flex-shrink: 0; + gap: 4px; +} + +.scm-view .monaco-list-row .history-item > .label-container > .codicon { + font-size: 14px; + border: 1px solid var(--vscode-scm-historyItemStatisticsBorder); + border-radius: 2px; + margin: 1px 0; + padding: 2px +} + .scm-view .monaco-list-row .history-item .stats-container { display: flex; font-size: 11px; @@ -332,10 +353,6 @@ border-radius: 2px; } -.scm-view .scm-editor-container .monaco-editor .focused .selected-text { - background-color: var(--vscode-editor-selectionBackground); -} - .scm-view .scm-editor { box-sizing: border-box; width: 100%; diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index 915abcb3e32..a5719651927 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -306,7 +306,7 @@ export class SCMHistoryProviderMenus implements ISCMHistoryProviderMenus, IDispo private getOutgoingHistoryItemGroupMenu(menuId: MenuId, historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu { const contextKeyService = this.contextKeyService.createOverlay([ - ['scmHistoryItemGroupHasUpstream', !!historyItemGroup.repository.provider.historyProvider?.currentHistoryItemGroup?.base], + ['scmHistoryItemGroupHasRemote', !!historyItemGroup.repository.provider.historyProvider?.currentHistoryItemGroup?.remote], ]); return this.menuService.createMenu(menuId, contextKeyService); diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index c60907964f0..91129c1d6ca 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -10,7 +10,7 @@ import { DirtyDiffWorkbenchController } from './dirtydiffDecorator'; import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { SCMActiveRepositoryContextKeyController, SCMActiveResourceContextKeyController, SCMStatusController } from './activity'; +import { SCMActiveResourceContextKeyController, SCMActiveRepositoryController } from './activity'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -113,13 +113,10 @@ viewsRegistry.registerViews([{ }], viewContainer); Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(SCMActiveResourceContextKeyController, LifecyclePhase.Restored); - -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(SCMActiveRepositoryContextKeyController, LifecyclePhase.Restored); + .registerWorkbenchContribution(SCMActiveRepositoryController, LifecyclePhase.Restored); Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(SCMStatusController, LifecyclePhase.Restored); + .registerWorkbenchContribution(SCMActiveResourceContextKeyController, LifecyclePhase.Restored); registerWorkbenchContribution2( SCMWorkingSetController.ID, @@ -352,6 +349,11 @@ Registry.as(ConfigurationExtensions.Configuration).regis ], description: localize('scm.workingSets.default', "Controls the default working set to use when switching to a source control history item group that does not have a working set."), default: 'current' + }, + 'scm.experimental.showHistoryGraph': { + type: 'boolean', + description: localize('scm.experimental.showHistoryGraph', "Controls whether to show the history graph instead of incoming/outgoing changes in the Source Control view."), + default: false } } }); @@ -386,6 +388,22 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'scm.clearInput', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(ContextKeyExpr.has('scmRepository'), SuggestContext.Visible.toNegated()), + primary: KeyCode.Escape, + handler: async (accessor) => { + const scmService = accessor.get(ISCMService); + const contextKeyService = accessor.get(IContextKeyService); + + const context = contextKeyService.getContext(getActiveElement()); + const repositoryId = context.getValue('scmRepository'); + const repository = repositoryId ? scmService.getRepository(repositoryId) : undefined; + repository?.input.setValue('', true); + } +}); + const viewNextCommitCommand = { description: { description: localize('scm view next commit', "Source Control: View Next Commit"), args: [] }, weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/contrib/scm/browser/scmHistory.ts b/src/vs/workbench/contrib/scm/browser/scmHistory.ts new file mode 100644 index 00000000000..f6505a2eec4 --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/scmHistory.ts @@ -0,0 +1,261 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { lastOrDefault } from 'vs/base/common/arrays'; +import { deepClone } from 'vs/base/common/objects'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemViewModel } from 'vs/workbench/contrib/scm/common/history'; + +const SWIMLANE_HEIGHT = 22; +const SWIMLANE_WIDTH = 11; +const CIRCLE_RADIUS = 4; +const SWIMLANE_CURVE_RADIUS = 5; + +const graphColors = ['#007ACC', '#BC3FBC', '#BF8803', '#CC6633', '#F14C4C', '#16825D']; + +function getNextColorIndex(colorIndex: number): number { + return colorIndex < graphColors.length - 1 ? colorIndex + 1 : 1; +} + +function getLabelColorIndex(historyItem: ISCMHistoryItem, colorMap: Map): number | undefined { + for (const label of historyItem.labels ?? []) { + const colorIndex = colorMap.get(label.title); + if (colorIndex !== undefined) { + return colorIndex; + } + } + + return undefined; +} + +function createPath(stroke: string): SVGPathElement { + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('fill', 'none'); + path.setAttribute('stroke', stroke); + path.setAttribute('stroke-width', '1px'); + path.setAttribute('stroke-linecap', 'round'); + + return path; +} + +function drawCircle(index: number, radius: number, fill: string): SVGCircleElement { + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + circle.setAttribute('cx', `${SWIMLANE_WIDTH * (index + 1)}`); + circle.setAttribute('cy', `${SWIMLANE_WIDTH}`); + circle.setAttribute('r', `${radius}`); + circle.setAttribute('fill', fill); + + return circle; +} + +function drawVerticalLine(x1: number, y1: number, y2: number, color: string): SVGPathElement { + const path = createPath(color); + path.setAttribute('d', `M ${x1} ${y1} V ${y2}`); + + return path; +} + +function findLastIndex(nodes: ISCMHistoryItemGraphNode[], id: string): number { + for (let i = nodes.length - 1; i >= 0; i--) { + if (nodes[i].id === id) { + return i; + } + } + + return -1; +} + +export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemViewModel): SVGElement { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.classList.add('graph'); + + const historyItem = historyItemViewModel.historyItem; + const inputSwimlanes = historyItemViewModel.inputSwimlanes; + const outputSwimlanes = historyItemViewModel.outputSwimlanes; + + // Find the history item in the input swimlanes + const inputIndex = inputSwimlanes.findIndex(node => node.id === historyItem.id); + + // Circle index - use the input swimlane index if present, otherwise add it to the end + const circleIndex = inputIndex !== -1 ? inputIndex : inputSwimlanes.length; + + // Circle color - use the output swimlane color if present, otherwise the input swimlane color + const circleColorIndex = circleIndex < outputSwimlanes.length ? outputSwimlanes[circleIndex].color : inputSwimlanes[circleIndex].color; + + let outputSwimlaneIndex = 0; + for (let index = 0; index < inputSwimlanes.length; index++) { + const color = graphColors[inputSwimlanes[index].color]; + + // Current commit + if (inputSwimlanes[index].id === historyItem.id) { + // Base commit + if (index !== circleIndex) { + const d: string[] = []; + const path = createPath(color); + + // Draw / + d.push(`M ${SWIMLANE_WIDTH * (index + 1)} 0`); + d.push(`A ${SWIMLANE_WIDTH} ${SWIMLANE_WIDTH} 0 0 1 ${SWIMLANE_WIDTH * (index)} ${SWIMLANE_WIDTH}`); + + // Draw - + d.push(`H ${SWIMLANE_WIDTH * (circleIndex + 1)}`); + + path.setAttribute('d', d.join(' ')); + svg.append(path); + } else { + outputSwimlaneIndex++; + } + } else { + // Not the current commit + if (outputSwimlaneIndex < outputSwimlanes.length && + inputSwimlanes[index].id === outputSwimlanes[outputSwimlaneIndex].id) { + if (index === outputSwimlaneIndex) { + // Draw | + const path = drawVerticalLine(SWIMLANE_WIDTH * (index + 1), 0, SWIMLANE_HEIGHT, color); + svg.append(path); + } else { + const d: string[] = []; + const path = createPath(color); + + // Draw | + d.push(`M ${SWIMLANE_WIDTH * (index + 1)} 0`); + d.push(`V 6`); + + // Draw / + d.push(`A ${SWIMLANE_CURVE_RADIUS} ${SWIMLANE_CURVE_RADIUS} 0 0 1 ${(SWIMLANE_WIDTH * (index + 1)) - SWIMLANE_CURVE_RADIUS} ${SWIMLANE_HEIGHT / 2}`); + + // Draw - + d.push(`H ${(SWIMLANE_WIDTH * (outputSwimlaneIndex + 1)) + SWIMLANE_CURVE_RADIUS}`); + + // Draw / + d.push(`A ${SWIMLANE_CURVE_RADIUS} ${SWIMLANE_CURVE_RADIUS} 0 0 0 ${SWIMLANE_WIDTH * (outputSwimlaneIndex + 1)} ${(SWIMLANE_HEIGHT / 2) + SWIMLANE_CURVE_RADIUS}`); + + // Draw | + d.push(`V ${SWIMLANE_HEIGHT}`); + + path.setAttribute('d', d.join(' ')); + svg.append(path); + } + + outputSwimlaneIndex++; + } + } + } + + // Add remaining parent(s) + for (let i = 1; i < historyItem.parentIds.length; i++) { + const parentOutputIndex = findLastIndex(outputSwimlanes, historyItem.parentIds[i]); + if (parentOutputIndex === -1) { + continue; + } + + // Draw -\ + const d: string[] = []; + const path = createPath(graphColors[outputSwimlanes[parentOutputIndex].color]); + + // Draw \ + d.push(`M ${SWIMLANE_WIDTH * parentOutputIndex} ${SWIMLANE_HEIGHT / 2}`); + d.push(`A ${SWIMLANE_WIDTH} ${SWIMLANE_WIDTH} 0 0 1 ${SWIMLANE_WIDTH * (parentOutputIndex + 1)} ${SWIMLANE_HEIGHT}`); + + // Draw - + d.push(`M ${SWIMLANE_WIDTH * parentOutputIndex} ${SWIMLANE_HEIGHT / 2}`); + d.push(`H ${SWIMLANE_WIDTH * (circleIndex + 1)} `); + + path.setAttribute('d', d.join(' ')); + svg.append(path); + } + + // Draw | to * + if (inputIndex !== -1) { + const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), 0, SWIMLANE_HEIGHT / 2, graphColors[inputSwimlanes[inputIndex].color]); + svg.append(path); + } + + // Draw | from * + if (historyItem.parentIds.length > 0) { + const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), SWIMLANE_HEIGHT / 2, SWIMLANE_HEIGHT, graphColors[circleColorIndex]); + svg.append(path); + } + + // Draw * + if (historyItem.parentIds.length > 1) { + // Multi-parent node + const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 1, graphColors[circleColorIndex]); + svg.append(circleOuter); + + const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, graphColors[circleColorIndex]); + svg.append(circleInner); + } else { + // HEAD + // TODO@lszomoru - implement a better way to determine if the commit is HEAD + if (historyItem.labels?.some(l => ThemeIcon.isThemeIcon(l.icon) && l.icon.id === 'target')) { + const outerCircle = drawCircle(circleIndex, CIRCLE_RADIUS + 2, graphColors[circleColorIndex]); + svg.append(outerCircle); + } + + // Node + const circle = drawCircle(circleIndex, CIRCLE_RADIUS, graphColors[circleColorIndex]); + svg.append(circle); + } + + // Set dimensions + svg.style.height = `${SWIMLANE_HEIGHT}px`; + svg.style.width = `${SWIMLANE_WIDTH * (Math.max(inputSwimlanes.length, outputSwimlanes.length, 1) + 1)}px`; + + return svg; +} + +export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], colorMap = new Map()): ISCMHistoryItemViewModel[] { + let colorIndex = -1; + const viewModels: ISCMHistoryItemViewModel[] = []; + + for (let index = 0; index < historyItems.length; index++) { + const historyItem = historyItems[index]; + + const outputSwimlanesFromPreviousItem = lastOrDefault(viewModels)?.outputSwimlanes ?? []; + const inputSwimlanes = outputSwimlanesFromPreviousItem.map(i => deepClone(i)); + const outputSwimlanes: ISCMHistoryItemGraphNode[] = []; + + if (historyItem.parentIds.length > 0) { + let firstParentAdded = false; + + // Add first parent to the output + for (const node of inputSwimlanes) { + if (node.id === historyItem.id) { + if (!firstParentAdded) { + outputSwimlanes.push({ + id: historyItem.parentIds[0], + color: getLabelColorIndex(historyItem, colorMap) ?? node.color + }); + firstParentAdded = true; + } + + continue; + } + + outputSwimlanes.push(deepClone(node)); + } + + // Add unprocessed parent(s) to the output + for (let i = firstParentAdded ? 1 : 0; i < historyItem.parentIds.length; i++) { + // Color index (label -> next color) + colorIndex = getLabelColorIndex(historyItem, colorMap) ?? getNextColorIndex(colorIndex); + + outputSwimlanes.push({ + id: historyItem.parentIds[i], + color: colorIndex + }); + } + } + + viewModels.push({ + historyItem, + inputSwimlanes, + outputSwimlanes, + }); + } + + return viewModels; +} diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts index e33a86c854a..594d24ef8f3 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts @@ -4,14 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/scm'; -import { IDisposable, DisposableStore, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; +import { autorun } from 'vs/base/common/observable'; import { append, $ } from 'vs/base/browser/dom'; import { ISCMProvider, ISCMRepository, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ActionRunner, IAction } from 'vs/base/common/actions'; -import { connectPrimaryMenu, isSCMRepository, StatusBarAction } from './util'; +import { connectPrimaryMenu, getRepositoryResourceCount, isSCMRepository, StatusBarAction } from './util'; import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { FuzzyScore } from 'vs/base/common/filters'; @@ -23,6 +24,9 @@ import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IManagedHover } from 'vs/base/browser/ui/hover/hover'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export class RepositoryActionRunner extends ActionRunner { constructor(private readonly getSelectedRepositories: () => ISCMRepository[]) { @@ -43,6 +47,7 @@ export class RepositoryActionRunner extends ActionRunner { interface RepositoryTemplate { readonly label: HTMLElement; + readonly labelCustomHover: IManagedHover; readonly name: HTMLElement; readonly description: HTMLElement; readonly countContainer: HTMLElement; @@ -64,6 +69,7 @@ export class RepositoryRenderer implements ICompressibleTreeRenderer provider.classList.toggle('active', e)); - const templateDisposable = combinedDisposable(visibilityDisposable, toolBar); + const templateDisposable = combinedDisposable(labelCustomHover, visibilityDisposable, toolBar); - return { label, name, description, countContainer, count, toolBar, elementDisposables: new DisposableStore(), templateDisposable }; + return { label, labelCustomHover, name, description, countContainer, count, toolBar, elementDisposables: new DisposableStore(), templateDisposable }; } renderElement(arg: ISCMRepository | ITreeNode, index: number, templateData: RepositoryTemplate, height: number | undefined): void { @@ -95,10 +102,10 @@ export class RepositoryRenderer implements ICompressibleTreeRenderer { - const commands = repository.provider.statusBarCommands || []; + templateData.elementDisposables.add(autorun(reader => { + const commands = repository.provider.statusBarCommands.read(reader) ?? []; statusPrimaryActions = commands.map(c => new StatusBarAction(c, this.commandService)); updateToolbar(); + })); - const count = repository.provider.count || 0; + templateData.elementDisposables.add(autorun(reader => { + const count = repository.provider.count.read(reader) ?? getRepositoryResourceCount(repository.provider); templateData.countContainer.setAttribute('data-count', String(count)); templateData.count.setCount(count); - }; - - // TODO@joao TODO@lszomoru - let disposed = false; - templateData.elementDisposables.add(toDisposable(() => disposed = true)); - templateData.elementDisposables.add(repository.provider.onDidChange(() => { - if (disposed) { - return; - } - - onDidChangeProvider(); })); - onDidChangeProvider(); - const repositoryMenus = this.scmViewService.menus.getRepositoryMenus(repository.provider); const menu = this.toolbarMenuId === MenuId.SCMTitle ? repositoryMenus.titleMenu.menu : repositoryMenus.repositoryMenu; templateData.elementDisposables.add(connectPrimaryMenu(menu, (primary, secondary) => { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 97455f3bc25..8b47b5cec4d 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -10,7 +10,7 @@ import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl, trackFocus, clearNode, prepend, isPointerEvent, isActiveElement } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryProviderCacheEntry, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemViewModel, SCMHistoryItemViewModelTreeElement, ISCMHistoryProviderCacheEntry, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, ISCMInputValueProviderContext, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; @@ -23,8 +23,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, MenuRegistry, Action2, IMenu } from 'vs/platform/actions/common/actions'; import { IAction, ActionRunner, Action, Separator, IActionRunner } from 'vs/base/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode, isSCMViewSeparator, connectPrimaryMenu } from './util'; +import { IThemeService, IFileIconTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode, isSCMViewSeparator, connectPrimaryMenu, isSCMHistoryItemViewModelTreeElement } from './util'; import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { disposableTimeout, Sequencer, ThrottledDelayer, Throttler } from 'vs/base/common/async'; @@ -96,19 +96,21 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { EditOperation } from 'vs/editor/common/core/editOperation'; import { stripIcons } from 'vs/base/common/iconLabels'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { foreground, listActiveSelectionForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; +import { editorSelectionBackground, foreground, inputBackground, inputForeground, listActiveSelectionForeground, registerColor, selectionBackground, transparent } from 'vs/platform/theme/common/colorRegistry'; import { IMenuWorkbenchToolBarOptions, MenuWorkbenchToolBar, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem'; import { clamp, rot } from 'vs/base/common/numbers'; import { ILogService } from 'vs/platform/log/common/log'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import type { IUpdatableHover, IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover, IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { OpenScmGroupAction } from 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; import { ITextModel } from 'vs/editor/common/model'; +import { autorun } from 'vs/base/common/observable'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory'; // type SCMResourceTreeNode = IResourceNode; // type SCMHistoryItemChangeResourceTreeNode = IResourceNode; @@ -121,39 +123,20 @@ type TreeElement = IResourceNode | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | + SCMHistoryItemViewModelTreeElement | SCMHistoryItemChangeTreeElement | IResourceNode | SCMViewSeparatorElement; type ShowChangesSetting = 'always' | 'never' | 'auto'; -registerColor('scm.historyItemAdditionsForeground', { - dark: 'gitDecoration.addedResourceForeground', - light: 'gitDecoration.addedResourceForeground', - hcDark: 'gitDecoration.addedResourceForeground', - hcLight: 'gitDecoration.addedResourceForeground' -}, localize('scm.historyItemAdditionsForeground', "History item additions foreground color.")); - -registerColor('scm.historyItemDeletionsForeground', { - dark: 'gitDecoration.deletedResourceForeground', - light: 'gitDecoration.deletedResourceForeground', - hcDark: 'gitDecoration.deletedResourceForeground', - hcLight: 'gitDecoration.deletedResourceForeground' -}, localize('scm.historyItemDeletionsForeground', "History item deletions foreground color.")); - -registerColor('scm.historyItemStatisticsBorder', { - dark: transparent(foreground, 0.2), - light: transparent(foreground, 0.2), - hcDark: transparent(foreground, 0.2), - hcLight: transparent(foreground, 0.2) -}, localize('scm.historyItemStatisticsBorder', "History item statistics border color.")); - -registerColor('scm.historyItemSelectedStatisticsBorder', { - dark: transparent(listActiveSelectionForeground, 0.2), - light: transparent(listActiveSelectionForeground, 0.2), - hcDark: transparent(listActiveSelectionForeground, 0.2), - hcLight: transparent(listActiveSelectionForeground, 0.2) -}, localize('scm.historyItemSelectedStatisticsBorder', "History item selected statistics border color.")); +registerColor('scm.historyItemAdditionsForeground', 'gitDecoration.addedResourceForeground', localize('scm.historyItemAdditionsForeground', "History item additions foreground color.")); + +registerColor('scm.historyItemDeletionsForeground', 'gitDecoration.deletedResourceForeground', localize('scm.historyItemDeletionsForeground', "History item deletions foreground color.")); + +registerColor('scm.historyItemStatisticsBorder', transparent(foreground, 0.2), localize('scm.historyItemStatisticsBorder', "History item statistics border color.")); + +registerColor('scm.historyItemSelectedStatisticsBorder', transparent(listActiveSelectionForeground, 0.2), localize('scm.historyItemSelectedStatisticsBorder', "History item selected statistics border color.")); function processResourceFilterData(uri: URI, filterData: FuzzyScore | LabelFuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] { if (!filterData) { @@ -861,7 +844,7 @@ interface HistoryItemTemplate { readonly iconContainer: HTMLElement; readonly label: IconLabel; readonly statsContainer: HTMLElement; - readonly statsCustomHover: IUpdatableHover; + readonly statsCustomHover: IManagedHover; readonly filesLabel: HTMLElement; readonly insertionsLabel: HTMLElement; readonly deletionsLabel: HTMLElement; @@ -901,7 +884,7 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer { + + static readonly TEMPLATE_ID = 'history-item-2'; + get templateId(): string { return HistoryItem2Renderer.TEMPLATE_ID; } + + constructor( + @IHoverService private readonly hoverService: IHoverService + ) { } + + renderTemplate(container: HTMLElement): HistoryItem2Template { + // hack + (container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-no-twistie'); + + const element = append(container, $('.history-item')); + const graphContainer = append(element, $('.graph-container')); + + const iconLabel = new IconLabel(element, { supportIcons: true, supportHighlights: true, supportDescriptionHighlights: true }); + + const labelContainer = append(element, $('.label-container')); + element.appendChild(labelContainer); + + return { graphContainer, label: iconLabel, labelContainer, elementDisposables: new DisposableStore(), disposables: new DisposableStore() }; + } + + renderElement(node: ITreeNode, index: number, templateData: HistoryItem2Template, height: number | undefined): void { + const historyItemViewModel = node.element.historyItemViewModel; + const historyItem = historyItemViewModel.historyItem; + + templateData.graphContainer.textContent = ''; + templateData.graphContainer.appendChild(renderSCMHistoryItemGraph(historyItemViewModel)); + + const title = this.getTooltip(historyItemViewModel); + const [matches, descriptionMatches] = this.processMatches(historyItemViewModel, node.filterData); + templateData.label.setLabel(historyItem.message, undefined, { title, matches, descriptionMatches }); + + templateData.labelContainer.textContent = ''; + if (historyItem.labels) { + for (const label of historyItem.labels) { + if (label.icon && ThemeIcon.isThemeIcon(label.icon)) { + const icon = append(templateData.labelContainer, $('div.label')); + icon.classList.add(...ThemeIcon.asClassNameArray(label.icon)); + + templateData.elementDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), icon, label.title)); + } + } + } + } + + renderCompressedElements(node: ITreeNode, LabelFuzzyScore>, index: number, templateData: HistoryItem2Template, height: number | undefined): void { + throw new Error('Should never happen since node is incompressible'); + } + + private getTooltip(historyItemViewModel: ISCMHistoryItemViewModel): IManagedHoverTooltipMarkdownString { + const historyItem = historyItemViewModel.historyItem; + const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); + + if (historyItem.author) { + markdown.appendMarkdown(`$(account) **${historyItem.author}**\n\n`); + } + + if (historyItem.timestamp) { + const dateFormatter = new Intl.DateTimeFormat(platform.language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); + markdown.appendMarkdown(`$(history) ${dateFormatter.format(historyItem.timestamp)}\n\n`); + } + + markdown.appendMarkdown(historyItem.message); + + return { markdown, markdownNotSupportedFallback: historyItem.message }; + } + + private processMatches(historyItemViewModel: ISCMHistoryItemViewModel, filterData: LabelFuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] { + if (!filterData) { + return [undefined, undefined]; + } + + return [ + historyItemViewModel.historyItem.message === filterData.label ? createMatches(filterData.score) : undefined, + historyItemViewModel.historyItem.author === filterData.label ? createMatches(filterData.score) : undefined + ]; + } + + disposeElement(element: ITreeNode, index: number, templateData: HistoryItem2Template, height: number | undefined): void { + templateData.elementDisposables.clear(); + } + + disposeTemplate(templateData: HistoryItem2Template): void { + templateData.disposables.dispose(); + } +} + interface HistoryItemChangeTemplate { readonly element: HTMLElement; readonly name: HTMLElement; @@ -1083,6 +1164,7 @@ class SeparatorRenderer implements ICompressibleTreeRenderer('scm.experimental.showHistoryGraph') !== true) { + const toolBar = new MenuWorkbenchToolBar(append(element, $('.actions')), MenuId.SCMChangesSeparator, { moreIcon: Codicon.gear }, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService); + disposables.add(toolBar); + } return { label, disposables }; } @@ -1149,6 +1233,8 @@ class ListDelegate implements IListVirtualDelegate { return HistoryItemGroupRenderer.TEMPLATE_ID; } else if (isSCMHistoryItemTreeElement(element)) { return HistoryItemRenderer.TEMPLATE_ID; + } else if (isSCMHistoryItemViewModelTreeElement(element)) { + return HistoryItem2Renderer.TEMPLATE_ID; } else if (isSCMHistoryItemChangeTreeElement(element) || isSCMHistoryItemChangeNode(element)) { return HistoryItemChangeRenderer.TEMPLATE_ID; } else if (isSCMViewSeparator(element)) { @@ -1229,6 +1315,10 @@ export class SCMTreeSorter implements ITreeSorter { return 0; } + if (isSCMHistoryItemViewModelTreeElement(one)) { + return isSCMHistoryItemViewModelTreeElement(other) ? 0 : 1; + } + if (isSCMHistoryItemChangeTreeElement(one) || isSCMHistoryItemChangeNode(one)) { // List if (this.viewMode() === ViewMode.List) { @@ -1313,6 +1403,11 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb // the author. A match in the message takes precedence over // a match in the author. return [element.message, element.author]; + } else if (isSCMHistoryItemViewModelTreeElement(element)) { + // For a history item we want to match both the message and + // the author. A match in the message takes precedence over + // a match in the author. + return [element.historyItemViewModel.historyItem.message, element.historyItemViewModel.historyItem.author]; } else if (isSCMViewSeparator(element)) { return element.label; } else { @@ -1363,6 +1458,10 @@ function getSCMResourceId(element: TreeElement): string { const historyItemGroup = element.historyItemGroup; const provider = historyItemGroup.repository.provider; return `historyItem:${provider.id}/${historyItemGroup.id}/${element.id}/${element.parentIds.join(',')}`; + } else if (isSCMHistoryItemViewModelTreeElement(element)) { + const provider = element.repository.provider; + const historyItem = element.historyItemViewModel.historyItem; + return `historyItem2:${provider.id}/${historyItem.id}/${historyItem.parentIds.join(',')}`; } else if (isSCMHistoryItemChangeTreeElement(element)) { const historyItem = element.historyItem; const historyItemGroup = historyItem.historyItemGroup; @@ -1413,6 +1512,9 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider { - if (typeof input.repository.provider.commitTemplate === 'undefined' || !input.visible) { + this.repositoryDisposables.add(autorun(reader => { + if (!input.visible) { return; } const oldCommitTemplate = commitTemplate; - commitTemplate = input.repository.provider.commitTemplate; + commitTemplate = input.repository.provider.commitTemplate.read(reader); const value = textModel.getValue(); - if (value && value !== oldCommitTemplate) { return; } textModel.setValue(commitTemplate); - }; - this.repositoryDisposables.add(input.repository.provider.onDidChangeCommitTemplate(updateTemplate, this)); - updateTemplate(); + })); // Update input enablement const updateEnablement = (enabled: boolean) => { @@ -2897,7 +2996,8 @@ export class SCMViewPane extends ViewPane { e.affectsConfiguration('scm.showActionButton') || e.affectsConfiguration('scm.showChangesSummary') || e.affectsConfiguration('scm.showIncomingChanges') || - e.affectsConfiguration('scm.showOutgoingChanges'), + e.affectsConfiguration('scm.showOutgoingChanges') || + e.affectsConfiguration('scm.experimental.showHistoryGraph'), this.visibilityDisposables) (() => this.updateChildren(), this, this.visibilityDisposables); @@ -2975,6 +3075,7 @@ export class SCMViewPane extends ViewPane { this.instantiationService.createInstance(ResourceRenderer, () => this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), resourceActionRunner), this.instantiationService.createInstance(HistoryItemGroupRenderer, historyItemGroupActionRunner), this.instantiationService.createInstance(HistoryItemRenderer, historyItemActionRunner, getActionViewItemProvider(this.instantiationService)), + this.instantiationService.createInstance(HistoryItem2Renderer), this.instantiationService.createInstance(HistoryItemChangeRenderer, () => this.viewMode, this.listLabels), this.instantiationService.createInstance(SeparatorRenderer) ], @@ -3100,6 +3201,25 @@ export class SCMViewPane extends ViewPane { } else if (isSCMHistoryItemTreeElement(e.element)) { this.scmViewService.focus(e.element.historyItemGroup.repository); return; + } else if (isSCMHistoryItemViewModelTreeElement(e.element)) { + const historyItem = e.element.historyItemViewModel.historyItem; + const historyItemParentId = historyItem.parentIds.length > 0 ? historyItem.parentIds[0] : undefined; + + const historyProvider = e.element.repository.provider.historyProvider; + const historyItemChanges = await historyProvider?.provideHistoryItemChanges(historyItem.id, historyItemParentId); + if (historyItemChanges) { + const title = `${historyItem.id.substring(0, 8)} - ${historyItem.message}`; + + const rootUri = e.element.repository.provider.rootUri; + const multiDiffSourceUri = rootUri ? + rootUri.with({ scheme: 'scm-history-item', path: `${rootUri.path}/${historyItem.id}` }) : + { scheme: 'scm-history-item', path: `${e.element.repository.provider.label}/${historyItem.id}` }; + + await this.commandService.executeCommand('_workbench.openMultiDiffEditor', { title, multiDiffSourceUri, resources: historyItemChanges }); + } + + this.scmViewService.focus(e.element.repository); + return; } else if (isSCMHistoryItemChangeTreeElement(e.element)) { if (e.element.originalUri && e.element.modifiedUri) { await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.uri, e.element.originalUri, e.element.modifiedUri), e); @@ -3414,8 +3534,8 @@ export class SCMViewPane extends ViewPane { return; } - this.isAnyRepositoryCollapsibleContextKey.set(this.scmViewService.visibleRepositories.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r))); - this.areAllRepositoriesCollapsedContextKey.set(this.scmViewService.visibleRepositories.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)))); + this.isAnyRepositoryCollapsibleContextKey.set(this.scmViewService.visibleRepositories.some(r => this.tree.hasNode(r) && this.tree.isCollapsible(r))); + this.areAllRepositoriesCollapsedContextKey.set(this.scmViewService.visibleRepositories.every(r => this.tree.hasNode(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)))); } collapseAllRepositories(): void { @@ -3603,6 +3723,8 @@ class SCMTreeDataSource implements IAsyncDataSource 0) { + const label = localize('syncSeparatorHeader', "Incoming/Outgoing"); + const ariaLabel = localize('syncSeparatorHeaderAriaLabel', "Incoming and outgoing changes"); + + children.push({ label, ariaLabel, repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement); + } + + children.push(...historyItems); + return children; } else if (isSCMResourceGroup(inputOrElement)) { if (this.viewMode() === ViewMode.List) { @@ -3703,13 +3836,13 @@ class SCMTreeDataSource implements IAsyncDataSource { - const { showIncomingChanges, showOutgoingChanges } = this.getConfiguration(); + const { showIncomingChanges, showOutgoingChanges, showHistoryGraph } = this.getConfiguration(); const scmProvider = element.provider; const historyProvider = scmProvider.historyProvider; const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; - if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never')) { + if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never') || showHistoryGraph) { return []; } @@ -3721,16 +3854,16 @@ class SCMTreeDataSource implements IAsyncDataSource { + const { showHistoryGraph } = this.getConfiguration(); + + const historyProvider = element.provider.historyProvider; + const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; + + if (!currentHistoryItemGroup || !showHistoryGraph) { + return []; + } + + const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(element); + let historyItemsElement = historyProviderCacheEntry.historyItems2.get(element.id); + const historyItemsMap = historyProviderCacheEntry.historyItems2; + + if (!historyItemsElement) { + const historyItemGroupIds = [ + currentHistoryItemGroup.id, + ...currentHistoryItemGroup.remote ? [currentHistoryItemGroup.remote.id] : [], + ...currentHistoryItemGroup.base ? [currentHistoryItemGroup.base.id] : [], + ]; + + historyItemsElement = await historyProvider.provideHistoryItems2({ historyItemGroupIds }) ?? []; + + this.historyProviderCache.set(element, { + ...historyProviderCacheEntry, + historyItems2: historyItemsMap.set(element.id, historyItemsElement) + }); + } + + // If we only have one history item that matches + // the current history item group, don't show it + if (historyItemsElement.length === 1 && + historyItemsElement[0].labels?.find(l => l.title === currentHistoryItemGroup.name)) { + return []; + } + + // Create the color map + // TODO@lszomoru - use theme colors + const colorMap = new Map([ + [currentHistoryItemGroup.name, 0] + ]); + if (currentHistoryItemGroup.remote) { + colorMap.set(currentHistoryItemGroup.remote.name, 1); + } + if (currentHistoryItemGroup.base) { + colorMap.set(currentHistoryItemGroup.base.name, 2); + } + + return toISCMHistoryItemViewModelArray(historyItemsElement, colorMap) + .map(historyItemViewModel => ({ + repository: element, + historyItemViewModel, + type: 'historyItem2' + }) satisfies SCMHistoryItemViewModelTreeElement); + } + private async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | IResourceNode)[]> { const repository = element.historyItemGroup.repository; const historyProvider = repository.provider.historyProvider; @@ -3921,13 +4110,15 @@ class SCMTreeDataSource implements IAsyncDataSource('scm.alwaysShowRepositories'), showActionButton: this.configurationService.getValue('scm.showActionButton'), showChangesSummary: this.configurationService.getValue('scm.showChangesSummary'), showIncomingChanges: this.configurationService.getValue('scm.showIncomingChanges'), - showOutgoingChanges: this.configurationService.getValue('scm.showOutgoingChanges') + showOutgoingChanges: this.configurationService.getValue('scm.showOutgoingChanges'), + showHistoryGraph: this.configurationService.getValue('scm.experimental.showHistoryGraph') }; } @@ -3965,6 +4156,7 @@ class SCMTreeDataSource implements IAsyncDataSource(), + historyItems2: new Map(), historyItemChanges: new Map() }; } @@ -4054,3 +4246,31 @@ export class SCMActionButton implements IDisposable { } } } + +// Override styles in selections.ts +registerThemingParticipant((theme, collector) => { + const selectionBackgroundColor = theme.getColor(selectionBackground); + + if (selectionBackgroundColor) { + // Override inactive selection bg + const inputBackgroundColor = theme.getColor(inputBackground); + if (inputBackgroundColor) { + collector.addRule(`.scm-view .scm-editor-container .monaco-editor .selected-text { background-color: ${inputBackgroundColor.transparent(0.4)}; }`); + } + + // Override selected fg + const inputForegroundColor = theme.getColor(inputForeground); + if (inputForegroundColor) { + collector.addRule(`.scm-view .scm-editor-container .monaco-editor .view-line span.inline-selected-text { color: ${inputForegroundColor}; }`); + } + + const backgroundColor = theme.getColor(inputBackground); + if (backgroundColor) { + collector.addRule(`.scm-view .scm-editor-container .monaco-editor-background { background-color: ${backgroundColor}; } `); + } + collector.addRule(`.scm-view .scm-editor-container .monaco-editor .focused .selected-text { background-color: ${selectionBackgroundColor}; }`); + } else { + // Use editor selection color if theme has not set a selection background color + collector.addRule(`.scm-view .scm-editor-container .monaco-editor .focused .selected-text { background-color: ${theme.getColor(editorSelectionBackground)}; }`); + } +}); diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 00c333886a6..ed24dc67bf0 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'vs/base/common/path'; -import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; +import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMHistoryItemViewModelTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton, ISCMViewService, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; import { IMenu, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -62,6 +62,10 @@ export function isSCMHistoryItemTreeElement(element: any): element is SCMHistory (element as SCMHistoryItemTreeElement).type === 'historyItem'; } +export function isSCMHistoryItemViewModelTreeElement(element: any): element is SCMHistoryItemViewModelTreeElement { + return (element as SCMHistoryItemViewModelTreeElement).type === 'historyItem2'; +} + export function isSCMHistoryItemChangeTreeElement(element: any): element is SCMHistoryItemChangeTreeElement { return (element as SCMHistoryItemChangeTreeElement).type === 'historyItemChange'; } @@ -173,3 +177,7 @@ export function getActionViewItemProvider(instaService: IInstantiationService): export function getProviderKey(provider: ISCMProvider): string { return `${provider.contextValue}:${provider.label}${provider.rootUri ? `:${provider.rootUri.toString()}` : ''}`; } + +export function getRepositoryResourceCount(provider: ISCMProvider): number { + return provider.groups.reduce((r, g) => r + g.resources.length, 0); +} diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 2cb81effd91..83d983824f3 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; +import { IObservable } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { IMenu } from 'vs/platform/actions/common/actions'; @@ -22,8 +23,10 @@ export interface ISCMHistoryProvider { get currentHistoryItemGroup(): ISCMHistoryItemGroup | undefined; set currentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined); + readonly currentHistoryItemGroupObs: IObservable; provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise; + provideHistoryItems2(options: ISCMHistoryOptions): Promise; provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise; resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined>; @@ -33,18 +36,21 @@ export interface ISCMHistoryProviderCacheEntry { readonly incomingHistoryItemGroup: SCMHistoryItemGroupTreeElement | undefined; readonly outgoingHistoryItemGroup: SCMHistoryItemGroupTreeElement | undefined; readonly historyItems: Map; + readonly historyItems2: Map; readonly historyItemChanges: Map; } export interface ISCMHistoryOptions { readonly cursor?: string; readonly limit?: number | { id?: string }; + readonly historyItemGroupIds?: readonly string[]; } export interface ISCMHistoryItemGroup { readonly id: string; readonly name: string; - readonly base?: Omit; + readonly base?: Omit, 'remote'>; + readonly remote?: Omit, 'remote'>; } export interface SCMHistoryItemGroupTreeElement { @@ -66,6 +72,11 @@ export interface ISCMHistoryItemStatistics { readonly deletions: number; } +export interface ISCMHistoryItemLabel { + readonly title: string; + readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; +} + export interface ISCMHistoryItem { readonly id: string; readonly parentIds: string[]; @@ -74,6 +85,24 @@ export interface ISCMHistoryItem { readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; readonly timestamp?: number; readonly statistics?: ISCMHistoryItemStatistics; + readonly labels?: ISCMHistoryItemLabel[]; +} + +export interface ISCMHistoryItemGraphNode { + readonly id: string; + readonly color: number; +} + +export interface ISCMHistoryItemViewModel { + readonly historyItem: ISCMHistoryItem; + readonly inputSwimlanes: ISCMHistoryItemGraphNode[]; + readonly outputSwimlanes: ISCMHistoryItemGraphNode[]; +} + +export interface SCMHistoryItemViewModelTreeElement { + readonly repository: ISCMRepository; + readonly historyItemViewModel: ISCMHistoryItemViewModel; + readonly type: 'historyItem2'; } export interface SCMHistoryItemTreeElement extends ISCMHistoryItem { diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 3fe568b77d5..5bcd1c6fbe7 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -15,6 +15,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { ResourceTree } from 'vs/base/common/resourceTree'; import { ISCMHistoryProvider, ISCMHistoryProviderMenus } from 'vs/workbench/contrib/scm/common/history'; import { ITextModel } from 'vs/editor/common/model'; +import { IObservable } from 'vs/base/common/observable'; export const VIEWLET_ID = 'workbench.view.scm'; export const VIEW_PANE_ID = 'workbench.scm'; @@ -72,15 +73,14 @@ export interface ISCMProvider extends IDisposable { readonly rootUri?: URI; readonly inputBoxTextModel: ITextModel; - readonly count?: number; - readonly commitTemplate: string; + readonly count: IObservable; + readonly commitTemplate: IObservable; readonly historyProvider?: ISCMHistoryProvider; - readonly onDidChangeCommitTemplate: Event; + readonly historyProviderObs: IObservable; readonly onDidChangeHistoryProvider: Event; - readonly onDidChangeStatusBarCommands?: Event; readonly acceptInputCommand?: Command; readonly actionButton?: ISCMActionButtonDescriptor; - readonly statusBarCommands?: readonly Command[]; + readonly statusBarCommands: IObservable; readonly onDidChange: Event; getOriginalResource(uri: URI): Promise; @@ -173,7 +173,9 @@ export interface ISCMService { readonly repositoryCount: number; registerSCMProvider(provider: ISCMProvider): ISCMRepository; + getRepository(id: string): ISCMRepository | undefined; + getRepository(resource: URI): ISCMRepository | undefined; } export interface ISCMTitleMenu { diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index 762dc9ed1b6..b341a2029ae 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator, ISCMInputChangeEvent, SCMInputChangeReason, InputValidationType, IInputValidation } from './scm'; import { ILogService } from 'vs/platform/log/common/log'; @@ -15,8 +15,10 @@ import { ResourceMap } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { Iterable } from 'vs/base/common/iterator'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { Schemas } from 'vs/base/common/network'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -class SCMInput implements ISCMInput { +class SCMInput extends Disposable implements ISCMInput { private _value = ''; @@ -104,9 +106,11 @@ class SCMInput implements ISCMInput { readonly repository: ISCMRepository, private readonly history: SCMInputHistory ) { + super(); + if (this.repository.provider.rootUri) { this.historyNavigator = history.getHistory(this.repository.provider.label, this.repository.provider.rootUri); - this.history.onWillSaveHistory(event => { + this._register(this.history.onWillSaveHistory(event => { if (this.historyNavigator.isAtEnd()) { this.saveValue(); } @@ -116,7 +120,7 @@ class SCMInput implements ISCMInput { } this.didChangeHistory = false; - }); + })); } else { // in memory only this.historyNavigator = new HistoryNavigator2([''], 100); } @@ -130,7 +134,7 @@ class SCMInput implements ISCMInput { } if (!transient) { - this.historyNavigator.add(this._value); + this.historyNavigator.replaceLast(this._value); this.historyNavigator.add(value); this.didChangeHistory = true; } @@ -362,7 +366,8 @@ export class SCMService implements ISCMService { @ILogService private readonly logService: ILogService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @IContextKeyService contextKeyService: IContextKeyService, - @IStorageService storageService: IStorageService + @IStorageService storageService: IStorageService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { this.inputHistory = new SCMInputHistory(storageService, workspaceContextService); this.providerCount = contextKeyService.createKey('scm.providerCount', 0); @@ -389,8 +394,36 @@ export class SCMService implements ISCMService { return repository; } - getRepository(id: string): ISCMRepository | undefined { - return this._repositories.get(id); - } + getRepository(id: string): ISCMRepository | undefined; + getRepository(resource: URI): ISCMRepository | undefined; + getRepository(idOrResource: string | URI): ISCMRepository | undefined { + if (typeof idOrResource === 'string') { + return this._repositories.get(idOrResource); + } + + if (idOrResource.scheme !== Schemas.file && + idOrResource.scheme !== Schemas.vscodeRemote) { + return undefined; + } + + let bestRepository: ISCMRepository | undefined = undefined; + let bestMatchLength = Number.POSITIVE_INFINITY; + + for (const repository of this.repositories) { + const root = repository.provider.rootUri; + if (!root) { + continue; + } + + const path = this.uriIdentityService.extUri.relativePath(root, idOrResource); + + if (path && !/^\.\./.test(path) && path.length < bestMatchLength) { + bestRepository = repository; + bestMatchLength = path.length; + } + } + + return bestRepository; + } } diff --git a/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts new file mode 100644 index 00000000000..5c64ccda402 --- /dev/null +++ b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts @@ -0,0 +1,503 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory'; +import { ISCMHistoryItem } from 'vs/workbench/contrib/scm/common/history'; + +suite('toISCMHistoryItemViewModelArray', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('empty graph', () => { + const viewModels = toISCMHistoryItemViewModelArray([]); + + assert.strictEqual(viewModels.length, 0); + }); + + + /** + * * a + */ + + test('single commit', () => { + const models = [ + { id: 'a', parentIds: [], message: '' }, + ] as ISCMHistoryItem[]; + + const viewModels = toISCMHistoryItemViewModelArray(models); + + assert.strictEqual(viewModels.length, 1); + + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + assert.strictEqual(viewModels[0].outputSwimlanes.length, 0); + }); + + /** + * * a(b) + * * b(c) + * * c(d) + * * d(e) + * * e + */ + test('linear graph', () => { + const models = [ + { id: 'a', parentIds: ['b'] }, + { id: 'b', parentIds: ['c'] }, + { id: 'c', parentIds: ['d'] }, + { id: 'd', parentIds: ['e'] }, + { id: 'e', parentIds: [] }, + ] as ISCMHistoryItem[]; + + const viewModels = toISCMHistoryItemViewModelArray(models); + + assert.strictEqual(viewModels.length, 5); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + + // node b + assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + + // node c + assert.strictEqual(viewModels[2].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + + // node d + assert.strictEqual(viewModels[3].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + + // node e + assert.strictEqual(viewModels[4].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 0); + }); + + /** + * * a(b) + * * b(c,d) + * |\ + * | * d(c) + * |/ + * * c(e) + * * e(f) + */ + test('merge commit (single commit in topic branch)', () => { + const models = [ + { id: 'a', parentIds: ['b'] }, + { id: 'b', parentIds: ['c', 'd'] }, + { id: 'd', parentIds: ['c'] }, + { id: 'c', parentIds: ['e'] }, + { id: 'e', parentIds: ['f'] }, + ] as ISCMHistoryItem[]; + + const viewModels = toISCMHistoryItemViewModelArray(models); + + assert.strictEqual(viewModels.length, 5); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + + // node b + assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + + // node d + assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1); + + // node c + assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + + // node e + assert.strictEqual(viewModels[4].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + }); + + /** + * * a(b,c) + * |\ + * | * c(d) + * * | b(e) + * * | e(f) + * * | f(d) + * |/ + * * d(g) + */ + test('merge commit (multiple commits in topic branch)', () => { + const models = [ + { id: 'a', parentIds: ['b', 'c'] }, + { id: 'c', parentIds: ['d'] }, + { id: 'b', parentIds: ['e'] }, + { id: 'e', parentIds: ['f'] }, + { id: 'f', parentIds: ['d'] }, + { id: 'd', parentIds: ['g'] }, + ] as ISCMHistoryItem[]; + + const viewModels = toISCMHistoryItemViewModelArray(models); + + assert.strictEqual(viewModels.length, 6); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1); + + // node c + assert.strictEqual(viewModels[1].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + + // node b + assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1); + + // node e + assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 1); + + // node f + assert.strictEqual(viewModels[4].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 1); + + // node d + assert.strictEqual(viewModels[5].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[5].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0); + }); + + /** + * * a(b,c) + * |\ + * | * c(b) + * |/ + * * b(d,e) + * |\ + * | * e(f) + * | * f(g) + * * | d(h) + */ + test('create brach from merge commit', () => { + const models = [ + { id: 'a', parentIds: ['b', 'c'] }, + { id: 'c', parentIds: ['b'] }, + { id: 'b', parentIds: ['d', 'e'] }, + { id: 'e', parentIds: ['f'] }, + { id: 'f', parentIds: ['g'] }, + { id: 'd', parentIds: ['h'] }, + ] as ISCMHistoryItem[]; + + const viewModels = toISCMHistoryItemViewModelArray(models); + + assert.strictEqual(viewModels.length, 6); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1); + + // node c + assert.strictEqual(viewModels[1].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'b'); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + + // node b + assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'b'); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 2); + + // node e + assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 2); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'f'); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 2); + + // node f + assert.strictEqual(viewModels[4].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'f'); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 2); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 2); + + // node d + assert.strictEqual(viewModels[5].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 2); + + assert.strictEqual(viewModels[5].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'h'); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[1].color, 2); + }); + + + /** + * * a(b,c) + * |\ + * | * c(d) + * * | b(e,f) + * |\| + * | |\ + * | | * f(g) + * * | | e(g) + * | * | d(g) + * |/ / + * | / + * |/ + * * g(h) + */ + test('create multiple branches from a commit', () => { + const models = [ + { id: 'a', parentIds: ['b', 'c'] }, + { id: 'c', parentIds: ['d'] }, + { id: 'b', parentIds: ['e', 'f'] }, + { id: 'f', parentIds: ['g'] }, + { id: 'e', parentIds: ['g'] }, + { id: 'd', parentIds: ['g'] }, + { id: 'g', parentIds: ['h'] }, + ] as ISCMHistoryItem[]; + + const viewModels = toISCMHistoryItemViewModelArray(models); + + assert.strictEqual(viewModels.length, 7); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1); + + // node c + assert.strictEqual(viewModels[1].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + + // node b + assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 3); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[2].outputSwimlanes[2].id, 'f'); + assert.strictEqual(viewModels[2].outputSwimlanes[2].color, 2); + + // node f + assert.strictEqual(viewModels[3].inputSwimlanes.length, 3); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[3].inputSwimlanes[2].id, 'f'); + assert.strictEqual(viewModels[3].inputSwimlanes[2].color, 2); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 3); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[3].outputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[3].outputSwimlanes[2].color, 2); + + // node e + assert.strictEqual(viewModels[4].inputSwimlanes.length, 3); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[4].inputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[4].inputSwimlanes[2].color, 2); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 3); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[4].outputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[4].outputSwimlanes[2].color, 2); + + // node d + assert.strictEqual(viewModels[5].inputSwimlanes.length, 3); + assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[5].inputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[5].inputSwimlanes[2].color, 2); + + assert.strictEqual(viewModels[5].outputSwimlanes.length, 3); + assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[5].outputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[2].color, 2); + + // node g + assert.strictEqual(viewModels[6].inputSwimlanes.length, 3); + assert.strictEqual(viewModels[6].inputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[6].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[6].inputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[6].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[6].inputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[6].inputSwimlanes[2].color, 2); + + assert.strictEqual(viewModels[6].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[6].outputSwimlanes[0].id, 'h'); + assert.strictEqual(viewModels[6].outputSwimlanes[0].color, 0); + }); +}); diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index c9a116bdbfe..74f304ba7a3 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -319,13 +319,12 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider | Promise> | FastAndSlowPicks { - const configuration = { ...this.configuration, includeSymbols: options.includeSymbols ?? this.configuration.includeSymbols }; const query = prepareQuery(filter); // Return early if we have editor symbol picks. We support this by: // - having a previously active global pick (e.g. a file) // - the user typing `@` to start the local symbol query - if (options.enableEditorSymbolSearch && options.includeSymbols) { + if (options.enableEditorSymbolSearch) { const editorSymbolPicks = this.getEditorSymbolPicks(query, disposables, token); if (editorSymbolPicks) { return editorSymbolPicks; @@ -397,7 +396,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider options.filter?.(p)); } @@ -406,7 +405,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider 0 ? [ - { type: 'separator', label: configuration.includeSymbols ? localize('fileAndSymbolResultsSeparator', "file and symbol results") : localize('fileResultsSeparator', "file results") }, + { type: 'separator', label: this.configuration.includeSymbols ? localize('fileAndSymbolResultsSeparator', "file and symbol results") : localize('fileResultsSeparator', "file results") }, ...additionalPicks ] : []; })(), diff --git a/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts b/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts index b39946294c8..4ef1cdae76e 100644 --- a/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts +++ b/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts @@ -13,7 +13,7 @@ import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/note import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { INotebookSearchService } from 'vs/workbench/contrib/search/common/notebookSearch'; import { INotebookCellMatchWithModel, INotebookFileMatchWithModel, contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers'; -import { ITextQuery, QueryType, ISearchProgressItem, ISearchComplete, ISearchConfigurationProperties, pathIncludedInQuery, ISearchService, IFolderQuery } from 'vs/workbench/services/search/common/search'; +import { ITextQuery, QueryType, ISearchProgressItem, ISearchComplete, ISearchConfigurationProperties, pathIncludedInQuery, ISearchService, IFolderQuery, DEFAULT_MAX_SEARCH_RESULTS } from 'vs/workbench/services/search/common/search'; import * as arrays from 'vs/base/common/arrays'; import { isNumber } from 'vs/base/common/types'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; @@ -232,7 +232,7 @@ export class NotebookSearchService implements INotebookSearchService { if (!widget.hasModel()) { continue; } - const askMax = isNumber(query.maxResults) ? query.maxResults + 1 : Number.MAX_SAFE_INTEGER; + const askMax = (isNumber(query.maxResults) ? query.maxResults : DEFAULT_MAX_SEARCH_RESULTS) + 1; const uri = widget.viewModel!.uri; if (!pathIncludedInQuery(query, uri.fsPath)) { diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 26e2d6cc45f..1da6d744da9 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -26,7 +26,7 @@ import { registerContributions as searchWidgetContributions } from 'vs/workbench import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; import { ISearchHistoryService, SearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; import { ISearchViewModelWorkbenchService, SearchViewModelWorkbenchService } from 'vs/workbench/contrib/search/browser/searchModel'; -import { SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, ViewMode, VIEW_ID } from 'vs/workbench/services/search/common/search'; +import { SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, ViewMode, VIEW_ID, DEFAULT_MAX_SEARCH_RESULTS } from 'vs/workbench/services/search/common/search'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { assertType } from 'vs/base/common/types'; import { getWorkspaceSymbols, IWorkspaceSymbol } from 'vs/workbench/contrib/search/common/search'; @@ -183,13 +183,13 @@ configurationRegistry.registerConfiguration({ }, 'search.useGlobalIgnoreFiles': { type: 'boolean', - markdownDescription: nls.localize('useGlobalIgnoreFiles', "Controls whether to use your global gitignore file (for example, from `$HOME/.config/git/ignore`) when searching for files. Requires `#search.useIgnoreFiles#` to be enabled."), + markdownDescription: nls.localize('useGlobalIgnoreFiles', "Controls whether to use your global gitignore file (for example, from `$HOME/.config/git/ignore`) when searching for files. Requires {0} to be enabled.", '`#search.useIgnoreFiles#`'), default: false, scope: ConfigurationScope.RESOURCE }, 'search.useParentIgnoreFiles': { type: 'boolean', - markdownDescription: nls.localize('useParentIgnoreFiles', "Controls whether to use `.gitignore` and `.ignore` files in parent directories when searching for files. Requires `#search.useIgnoreFiles#` to be enabled."), + markdownDescription: nls.localize('useParentIgnoreFiles', "Controls whether to use `.gitignore` and `.ignore` files in parent directories when searching for files. Requires {0} to be enabled.", '`#search.useIgnoreFiles#`'), default: false, scope: ConfigurationScope.RESOURCE }, @@ -198,6 +198,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize('search.quickOpen.includeSymbols', "Whether to include results from a global symbol search in the file results for Quick Open."), default: false }, + 'search.ripgrep.maxThreads': { + type: 'number', + description: nls.localize('search.ripgrep.maxThreads', "Number of threads to use for searching. When set to 0, the engine automatically determines this value."), + default: 0 + }, 'search.quickOpen.includeHistory': { type: 'boolean', description: nls.localize('search.quickOpen.includeHistory', "Whether to include results from recently opened files in the file results for Quick Open."), @@ -238,7 +243,7 @@ configurationRegistry.registerConfiguration({ }, 'search.maxResults': { type: ['number', 'null'], - default: 20000, + default: DEFAULT_MAX_SEARCH_RESULTS, markdownDescription: nls.localize('search.maxResults', "Controls the maximum number of search results, this can be set to `null` (empty) to return unlimited results.") }, 'search.collapseResults': { diff --git a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts index e848ced9bb3..a16f5a19f44 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts @@ -49,6 +49,7 @@ export interface IFindInFilesArgs { matchWholeWord?: boolean; useExcludeSettingsAndIgnoreFiles?: boolean; onlyOpenEditors?: boolean; + showIncludesExcludes?: boolean; } //#endregion @@ -208,6 +209,7 @@ registerAction2(class FindInFilesAction extends Action2 { matchWholeWord: { 'type': 'boolean' }, useExcludeSettingsAndIgnoreFiles: { 'type': 'boolean' }, onlyOpenEditors: { 'type': 'boolean' }, + showIncludesExcludes: { 'type': 'boolean' } } } }, @@ -407,6 +409,9 @@ export async function findInFilesCommand(accessor: ServicesAccessor, _args: IFin updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: typeof args.replace !== 'string' }); } openedView.setSearchParameters(args); + if (typeof args.showIncludesExcludes === 'boolean') { + openedView.toggleQueryDetails(false, args.showIncludesExcludes); + } openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); } diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 82c166d796d..764f8966664 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -41,7 +41,7 @@ import { contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches, I import { INotebookSearchService } from 'vs/workbench/contrib/search/common/notebookSearch'; import { rawCellPrefix, INotebookCellMatchNoModel, isINotebookFileMatchNoModel } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { ReplacePattern } from 'vs/workbench/services/search/common/replace'; -import { IAITextQuery, IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, QueryType, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; +import { DEFAULT_MAX_SEARCH_RESULTS, IAITextQuery, IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, QueryType, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { getTextSearchMatchWithModelContext, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchModel'; import { CellFindMatchModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; @@ -529,7 +529,7 @@ export class FileMatch extends Disposable implements IFileMatch { const wordSeparators = this._query.isWordMatch && this._query.wordSeparators ? this._query.wordSeparators : null; const matches = this._model - .findMatches(this._query.pattern, this._model.getFullModelRange(), !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); + .findMatches(this._query.pattern, this._model.getFullModelRange(), !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? DEFAULT_MAX_SEARCH_RESULTS); this.updateMatches(matches, true, this._model, false); } @@ -550,7 +550,7 @@ export class FileMatch extends Disposable implements IFileMatch { oldMatches.forEach(match => this._textMatches.delete(match.id())); const wordSeparators = this._query.isWordMatch && this._query.wordSeparators ? this._query.wordSeparators : null; - const matches = this._model.findMatches(this._query.pattern, range, !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); + const matches = this._model.findMatches(this._query.pattern, range, !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? DEFAULT_MAX_SEARCH_RESULTS); this.updateMatches(matches, modelChange, this._model, false); // await this.updateMatchesForEditorWidget(); diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index d7c088c442e..121512e94be 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -136,7 +136,7 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree SearchContext.FileFocusKey.bindTo(contextKeyServiceMain).set(false); SearchContext.FolderFocusKey.bindTo(contextKeyServiceMain).set(true); - const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain])); + const instantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain]))); const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { menuOptions: { shouldForwardArgs: true @@ -365,7 +365,7 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender templateData.after.textContent = preview.after; const title = (preview.fullBefore + (replace ? match.replaceString : preview.inside) + preview.after).trim().substr(0, 999); - templateData.disposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), templateData.parent, title)); + templateData.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), templateData.parent, title)); SearchContext.IsEditableItemKey.bindTo(templateData.contextKeyService).set(!(match instanceof MatchInNotebook && match.isReadonly())); @@ -377,7 +377,7 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender templateData.lineNumber.classList.toggle('show', (numLines > 0) || showLineNumbers); templateData.lineNumber.textContent = lineNumberStr + extraLinesStr; - templateData.disposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), templateData.lineNumber, this.getMatchTitle(match, showLineNumbers))); + templateData.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), templateData.lineNumber, this.getMatchTitle(match, showLineNumbers))); templateData.actions.context = { viewer: this.searchView.getControl(), element: match } satisfies ISearchActionContext; diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 69e8a910540..662d7fb9019 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -166,6 +166,9 @@ export class SearchView extends ViewPane { private _onSearchResultChangedDisposable: IDisposable | undefined; + private _stashedQueryDetailsVisibility: boolean | undefined = undefined; + private _stashedReplaceVisibility: boolean | undefined = undefined; + constructor( options: IViewPaneOptions, @IFileService private readonly fileService: IFileService, @@ -237,8 +240,8 @@ export class SearchView extends ViewPane { this.inputPatternExclusionsFocused = Constants.SearchContext.PatternExcludesFocusedKey.bindTo(this.contextKeyService); this.isEditableItem = Constants.SearchContext.IsEditableItemKey.bindTo(this.contextKeyService); - this.instantiationService = this.instantiationService.createChild( - new ServiceCollection([IContextKeyService, this.contextKeyService])); + this.instantiationService = this._register(this.instantiationService.createChild( + new ServiceCollection([IContextKeyService, this.contextKeyService]))); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('search.sortOrder')) { @@ -327,7 +330,32 @@ export class SearchView extends ViewPane { if (visible === this.aiResultsVisible) { return; } + + if (visible) { + this._stashedQueryDetailsVisibility = this._queryDetailsHidden(); + this._stashedReplaceVisibility = this.searchWidget.isReplaceShown(); + + this.searchWidget.toggleReplace(false); + this.toggleQueryDetailsButton.style.display = 'none'; + + this.searchWidget.replaceButtonVisibility = false; + this.toggleQueryDetails(undefined, false); + } else { + this.toggleQueryDetailsButton.style.display = ''; + this.searchWidget.replaceButtonVisibility = true; + + if (this._stashedReplaceVisibility) { + this.searchWidget.toggleReplace(this._stashedReplaceVisibility); + } + + if (this._stashedQueryDetailsVisibility) { + this.toggleQueryDetails(undefined, this._stashedQueryDetailsVisibility); + } + } + this.aiResultsVisible = visible; + + if (this.viewModel.searchResult.isEmpty()) { return; } @@ -336,9 +364,8 @@ export class SearchView extends ViewPane { this.model.cancelAISearch(); if (visible) { await this.model.addAIResults(); - } else { - this.searchWidget.toggleReplace(false); } + this.onSearchResultsChanged(); this.onSearchComplete(() => { }, undefined, undefined, this.viewModel.searchResult.getCachedSearchComplete(visible)); } @@ -455,7 +482,7 @@ export class SearchView extends ViewPane { // Toggle query details button this.toggleQueryDetailsButton = dom.append(this.queryDetails, $('.more' + ThemeIcon.asCSSSelector(searchDetailsIcon), { tabindex: 0, role: 'button' })); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, nls.localize('moreSearch', "Toggle Search Details"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, nls.localize('moreSearch', "Toggle Search Details"))); this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.CLICK, e => { dom.EventHelper.stop(e); @@ -1482,6 +1509,10 @@ export class SearchView extends ViewPane { } } + private _queryDetailsHidden() { + return this.queryDetails.classList.contains('more'); + } + searchInFolders(folderPaths: string[] = []): void { this._searchWithIncludeOrExclude(true, folderPaths); } @@ -2222,7 +2253,7 @@ class SearchLinkButton extends Disposable { constructor(label: string, handler: (e: dom.EventLike) => unknown, hoverService: IHoverService, tooltip?: string) { super(); this.element = $('a.pointer', { tabindex: 0 }, label); - this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.element, tooltip)); + this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, tooltip)); this.addEventHandlers(handler); } diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index cf86112b1c2..f8beab04b54 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -16,7 +17,6 @@ import { Delayer } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { CONTEXT_FIND_WIDGET_NOT_VISIBLE } from 'vs/editor/contrib/find/browser/findModel'; -import * as nls from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -44,6 +44,7 @@ import { GroupModelChangeKind } from 'vs/workbench/common/editor'; import { SearchFindInput } from 'vs/workbench/contrib/search/browser/searchFindInput'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { NotebookFindScopeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; /** Specified in searchview.css */ const SingleLineInputHeight = 26; @@ -205,8 +206,7 @@ export class SearchWidget extends Widget { notebookOptions.isInNotebookMarkdownPreview, notebookOptions.isInNotebookCellInput, notebookOptions.isInNotebookCellOutput, - false, - [] + { findScopeType: NotebookFindScopeType.None } )); this._register( @@ -352,6 +352,12 @@ export class SearchWidget extends Widget { this.searchInput?.focusOnRegex(); } + set replaceButtonVisibility(val: boolean) { + if (this.toggleReplaceButton) { + this.toggleReplaceButton.element.style.display = val ? '' : 'none'; + } + } + private render(container: HTMLElement, options: ISearchWidgetOptions): void { this.domNode = dom.append(container, dom.$('.search-widget')); this.domNode.style.position = 'relative'; @@ -523,7 +529,7 @@ export class SearchWidget extends Widget { } })); - this.replaceInput.onKeyDown((keyboardEvent) => this.onReplaceInputKeyDown(keyboardEvent)); + this._register(this.replaceInput.onKeyDown((keyboardEvent) => this.onReplaceInputKeyDown(keyboardEvent))); this.replaceInput.setValue(options.replaceValue || ''); this._register(this.replaceInput.inputBox.onDidChange(() => this._onReplaceValueChanged.fire())); this._register(this.replaceInput.inputBox.onDidHeightChange(() => this._onDidHeightChange.fire())); @@ -665,6 +671,25 @@ export class SearchWidget extends Widget { else if (keyboardEvent.equals(KeyCode.DownArrow)) { stopPropagationForMultiLineDownwards(keyboardEvent, this.searchInput?.getValue() ?? '', this.searchInput?.domNode.querySelector('textarea') ?? null); } + + else if (keyboardEvent.equals(KeyCode.PageUp)) { + const inputElement = this.searchInput?.inputBox.inputElement; + if (inputElement) { + inputElement.setSelectionRange(0, 0); + inputElement.focus(); + keyboardEvent.preventDefault(); + } + } + + else if (keyboardEvent.equals(KeyCode.PageDown)) { + const inputElement = this.searchInput?.inputBox.inputElement; + if (inputElement) { + const endOfText = inputElement.value.length; + inputElement.setSelectionRange(endOfText, endOfText); + inputElement.focus(); + keyboardEvent.preventDefault(); + } + } } private onCaseSensitiveKeyDown(keyboardEvent: IKeyboardEvent) { diff --git a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index 9b3b9e4f4dd..2e98bb36cb6 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Keybinding } from 'vs/base/common/keybindings'; import { OS } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts b/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts index 17f9e88b338..5c79a6d8046 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import * as arrays from 'vs/base/common/arrays'; import { DeferredPromise, timeout } from 'vs/base/common/async'; @@ -644,4 +644,3 @@ suite('SearchModel', () => { return notebookEditorWidgetService; } }); - diff --git a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts index a90875c1384..f30dcaf0de3 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; import { FindMatch, IReadonlyTextBuffer } from 'vs/editor/common/model'; import { IFileMatch, ISearchRange, ITextSearchMatch, QueryType } from 'vs/workbench/services/search/common/search'; diff --git a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts index e71fab4b131..600cc67f97a 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { Match, FileMatch, SearchResult, SearchModel, FolderMatch, CellMatch } from 'vs/workbench/contrib/search/browser/searchModel'; diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index 4feb3d343ed..c35d9b87da6 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IModelService } from 'vs/editor/common/services/model'; diff --git a/src/vs/workbench/contrib/search/test/common/cacheState.test.ts b/src/vs/workbench/contrib/search/test/common/cacheState.test.ts index a986de9b0bf..ea1241bed90 100644 --- a/src/vs/workbench/contrib/search/test/common/cacheState.test.ts +++ b/src/vs/workbench/contrib/search/test/common/cacheState.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as errors from 'vs/base/common/errors'; import { QueryType, IFileQuery } from 'vs/workbench/services/search/common/search'; import { FileQueryCacheState } from 'vs/workbench/contrib/search/common/cacheState'; diff --git a/src/vs/workbench/contrib/search/test/common/extractRange.test.ts b/src/vs/workbench/contrib/search/test/common/extractRange.test.ts index 4651bbc6403..0758b486cf6 100644 --- a/src/vs/workbench/contrib/search/test/common/extractRange.test.ts +++ b/src/vs/workbench/contrib/search/test/common/extractRange.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { extractRangeFromFilter } from 'vs/workbench/contrib/search/common/search'; @@ -98,4 +98,3 @@ suite('extractRangeFromFilter', () => { } }); }); - diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index c0f0f27dbfa..c3577b4e873 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -141,7 +141,7 @@ export class SearchEditor extends AbstractTextCodeEditor this.createQueryEditor( this.queryEditorContainer, - this.instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])), + this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService]))), SearchContext.InputBoxFocusedKey.bindTo(scopedContextKeyService) ); } @@ -166,7 +166,7 @@ export class SearchEditor extends AbstractTextCodeEditor // Toggle query details button this.toggleQueryDetailsButton = DOM.append(this.includesExcludesContainer, DOM.$('.expand' + ThemeIcon.asCSSSelector(searchDetailsIcon), { tabindex: 0, role: 'button' })); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, localize('moreSearch', "Toggle Search Details"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, localize('moreSearch', "Toggle Search Details"))); this._register(DOM.addDisposableListener(this.toggleQueryDetailsButton, DOM.EventType.CLICK, e => { DOM.EventHelper.stop(e); this.toggleIncludesExcludes(); @@ -776,7 +776,7 @@ export class SearchEditor extends AbstractTextCodeEditor } } -const searchEditorTextInputBorder = registerColor('searchEditor.textInputBorder', { dark: inputBorder, light: inputBorder, hcDark: inputBorder, hcLight: inputBorder }, localize('textInputBoxBorder', "Search editor text input box border.")); +const searchEditorTextInputBorder = registerColor('searchEditor.textInputBorder', inputBorder, localize('textInputBoxBorder', "Search editor text input box border.")); function findNextRange(matchRanges: Range[], currentPosition: Position) { for (const matchRange of matchRanges) { diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts index 82e14ccc1a6..6b5bef90c2a 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { SnippetFile, Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { URI } from 'vs/base/common/uri'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsRegistry.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsRegistry.test.ts index 8c185e766a5..d4824fdf3b9 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsRegistry.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsRegistry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { getNonWhitespacePrefix } from 'vs/workbench/contrib/snippets/browser/snippetsService'; import { Position } from 'vs/editor/common/core/position'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsRewrite.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsRewrite.test.ts index 5ac2018ef7d..39b490f2fe3 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsRewrite.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsRewrite.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { generateUuid } from 'vs/base/common/uuid'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index ce61496ec51..297836af6c8 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { SnippetCompletion, SnippetCompletionProvider } from 'vs/workbench/contrib/snippets/browser/snippetCompletionProvider'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; diff --git a/src/vs/workbench/contrib/speech/test/common/speechService.test.ts b/src/vs/workbench/contrib/speech/test/common/speechService.test.ts index d757eace7e0..16a4f0d9b77 100644 --- a/src/vs/workbench/contrib/speech/test/common/speechService.test.ts +++ b/src/vs/workbench/contrib/speech/test/common/speechService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { speechLanguageConfigToLanguage } from 'vs/workbench/contrib/speech/common/speechService'; diff --git a/src/vs/workbench/contrib/splash/browser/partsSplash.ts b/src/vs/workbench/contrib/splash/browser/partsSplash.ts index 6982a2cd50b..8b2dd0dcdd1 100644 --- a/src/vs/workbench/contrib/splash/browser/partsSplash.ts +++ b/src/vs/workbench/contrib/splash/browser/partsSplash.ts @@ -110,8 +110,6 @@ export class PartsSplash { // remove initial colors const defaultStyles = mainWindow.document.head.getElementsByClassName('initialShellColors'); - if (defaultStyles.length) { - mainWindow.document.head.removeChild(defaultStyles[0]); - } + defaultStyles[0]?.remove(); } } diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts index b560ed8999b..1fd7dd05886 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts @@ -178,7 +178,59 @@ const ModulesToLookFor = [ '@azure/web-pubsub-express', '@azure/openai', '@azure/arm-hybridkubernetes', - '@azure/arm-kubernetesconfiguration' + '@azure/arm-kubernetesconfiguration', + //AI and vector db dev packages + '@anthropic-ai/sdk', + '@anthropic-ai/tokenizer', + '@arizeai/openinference-instrumentation-langchain', + '@arizeai/openinference-instrumentation-openai', + '@aws-sdk-client-bedrock-runtime', + '@aws-sdk/client-bedrock', + '@datastax/astra-db-ts', + 'fireworks-js', + '@google-cloud/aiplatform', + '@huggingface/inference', + 'humanloop', + '@langchain/anthropic', + 'langsmith', + 'llamaindex', + 'mongodb', + 'neo4j-driver', + 'ollama', + 'onnxruntime-node', + 'onnxruntime-web', + 'pg', + 'postgresql', + 'redis', + '@supabase/supabase-js', + '@tensorflow/tfjs', + '@xenova/transformers', + 'tika', + 'weaviate-client', + '@zilliz/milvus2-sdk-node', + //Azure AI + '@azure-rest/ai-anomaly-detector', + '@azure-rest/ai-content-safety', + '@azure-rest/ai-document-intelligence', + '@azure-rest/ai-document-translator', + '@azure-rest/ai-personalizer', + '@azure-rest/ai-translation-text', + '@azure-rest/ai-vision-image-analysis', + '@azure/ai-anomaly-detector', + '@azure/ai-form-recognizer', + '@azure/ai-language-conversations', + '@azure/ai-language-text', + '@azure/ai-text-analytics', + '@azure/arm-botservice', + '@azure/arm-cognitiveservices', + '@azure/arm-machinelearning', + '@azure/cognitiveservices-contentmoderator', + '@azure/cognitiveservices-customvision-prediction', + '@azure/cognitiveservices-customvision-training', + '@azure/cognitiveservices-face', + '@azure/cognitiveservices-translatortext', + 'microsoft-cognitiveservices-speech-sdk' + ]; const PyMetaModulesToLookFor = [ @@ -311,7 +363,38 @@ const PyModulesToLookFor = [ 'guidance', 'openai', 'semantic-kernel', - 'sentence-transformers' + 'sentence-transformers', + // AI and vector db dev packages + 'anthropic', + 'aporia', + 'arize', + 'deepchecks', + 'fireworks-ai', + 'langchain-fireworks', + 'humanloop', + 'pymongo', + 'langchain-anthropic', + 'langchain-huggingface', + 'langchain-fireworks', + 'ollama', + 'onnxruntime', + 'pgvector', + 'sentence-transformers', + 'tika', + 'trulens', + 'trulens-eval', + 'wandb', + // Azure AI Services + 'azure-ai-contentsafety', + 'azure-ai-documentintelligence', + 'azure-ai-translation-text', + 'azure-ai-vision', + 'azure-cognitiveservices-language-luis', + 'azure-cognitiveservices-speech', + 'azure-cognitiveservices-vision-contentmoderator', + 'azure-cognitiveservices-vision-face', + 'azure-mgmt-cognitiveservices', + 'azure-mgmt-search' ]; const GoModulesToLookFor = [ @@ -428,6 +511,12 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.react" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@angular/core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.vue" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@anthropic-ai/sdk" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@anthropic-ai/tokenizer" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@arizeai/openinference-instrumentation-langchain" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@arizeai/openinference-instrumentation-openai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@aws-sdk-client-bedrock-runtime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@aws-sdk/client-bedrock" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.aws-sdk" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.aws-amplify-sdk" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -439,11 +528,13 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.@azure/keyvault" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/search" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/storage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@google-cloud/aiplatform" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.azure" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.azure-storage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@google-cloud/common" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.firebase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.heroku-cli" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@huggingface/inference" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@microsoft/teams-js" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@microsoft/office-js" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@microsoft/office-js-helpers" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -470,15 +561,35 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.cypress" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.chroma" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.faiss" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.fireworks-js" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@datastax/astra-db-ts" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.humanloop" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.langchain" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@langchain/anthropic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.langsmith" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.llamaindex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.milvus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.mongodb" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.neo4j-driver" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.ollama" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.onnxruntime-node" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.onnxruntime-web" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.openai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.pinecone" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.postgresql" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.pg" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.qdrant" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.redis" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@supabase/supabase-js" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@tensorflow/tfjs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@xenova/transformers" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.weaviate-client" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@zilliz/milvus2-sdk-node" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.nightwatch" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.protractor" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.puppeteer" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.selenium-webdriver" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.tika" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.webdriverio" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.gherkin" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/app-configuration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -490,6 +601,27 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.@azure/synapse-artifacts" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/synapse-access-control" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/ai-metrics-advisor" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-anomaly-detector" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-content-safety" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-document-intelligence" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-document-translator" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-personalizer" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-translation-text" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-vision-image-analysis" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/ai-anomaly-detector" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/ai-form-recognizer" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/ai-language-conversations" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/ai-language-text" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/ai-text-analytics" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/arm-botservice" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/arm-cognitiveservices" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/arm-machinelearning" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/cognitiveservices-contentmoderator" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/cognitiveservices-customvision-prediction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/cognitiveservices-customvision-training" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/cognitiveservices-face" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/cognitiveservices-translatortext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.microsoft-cognitiveservices-speech-sdk" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/service-bus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/keyvault-secrets" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/keyvault-keys" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -630,6 +762,16 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.azure-ai-language-conversations" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-ai-language-questionanswering" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-ai-ml" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-ai-contentsafety" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-ai-documentintelligence" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-ai-translation-text" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-ai-vision" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-cognitiveservices-language-luis" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-cognitiveservices-speech" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-cognitiveservices-vision-contentmoderator" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-cognitiveservices-vision-face" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-mgmt-cognitiveservices" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-mgmt-search" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-ai-translation-document" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-cognitiveservices" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -745,13 +887,30 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.azure-messaging-webpubsubservice" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-data-nspkg" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-data-tables" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.arize" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.aporia" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.anthropic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.deepchecks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.fireworks-ai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.transformers" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.humanloop" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.langchain" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.langchain-anthropic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.langchain-fireworks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.langchain-huggingface" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.llama-index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.guidance" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.ollama" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.onnxruntime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.openai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.pymongo" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.pgvector" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.semantic-kernel" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.sentence-transformers" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.tika" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.trulens" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.trulens-eval" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.wandb" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/storage/azfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, diff --git a/src/vs/workbench/contrib/tags/test/node/workspaceTags.test.ts b/src/vs/workbench/contrib/tags/test/node/workspaceTags.test.ts index 13993422b73..d166a151900 100644 --- a/src/vs/workbench/contrib/tags/test/node/workspaceTags.test.ts +++ b/src/vs/workbench/contrib/tags/test/node/workspaceTags.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as crypto from 'crypto'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getHashedRemotesFromConfig as baseGetHashedRemotesFromConfig } from 'vs/workbench/contrib/tags/common/workspaceTags'; diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 0e9b7879ba0..e11e8a81fc3 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -3241,7 +3241,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer let configFileCreated = false; this._fileService.stat(resource).then((stat) => stat, () => undefined).then(async (stat) => { const fileExists: boolean = !!stat; - const configValue = this._configurationService.inspect('tasks'); + const configValue = this._configurationService.inspect('tasks', { resource }); let tasksExistInFile: boolean; let target: ConfigurationTarget; switch (taskSource) { diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 08e8292b60c..ac6fdcba385 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -23,7 +23,7 @@ import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/se import { ITaskEvent, TaskEventKind, TaskGroup, TaskSettingId, TASKS_CATEGORY, TASK_RUNNING_STATE } from 'vs/workbench/contrib/tasks/common/tasks'; import { ITaskService, TaskCommandsRegistered, TaskExecutionSupportedContext } from 'vs/workbench/contrib/tasks/common/taskService'; -import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { RunAutomaticTasks, ManageAutomaticTaskRunning } from 'vs/workbench/contrib/tasks/browser/runAutomaticTasks'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; @@ -109,7 +109,7 @@ export class TaskStatusBarContributions extends Disposable implements IWorkbench } if (promise && (event.kind === TaskEventKind.Active) && (this._activeTasksCount === 1)) { - this._progressService.withProgress({ location: ProgressLocation.Window, command: 'workbench.action.tasks.showTasks', type: 'loading' }, progress => { + this._progressService.withProgress({ location: ProgressLocation.Window, command: 'workbench.action.tasks.showTasks' }, progress => { progress.report({ message: nls.localize('building', 'Building...') }); return promise!; }).then(() => { @@ -431,15 +431,24 @@ schema.oneOf = [...(schemaVersion2.oneOf || []), ...(schemaVersion1.oneOf || []) const jsonRegistry = Registry.as(jsonContributionRegistry.Extensions.JSONContribution); jsonRegistry.registerSchema(tasksSchemaId, schema); -ProblemMatcherRegistry.onMatcherChanged(() => { - updateProblemMatchers(); - jsonRegistry.notifySchemaChanged(tasksSchemaId); -}); +export class TaskRegistryContribution extends Disposable implements IWorkbenchContribution { + static ID = 'taskRegistryContribution'; + constructor() { + super(); + + this._register(ProblemMatcherRegistry.onMatcherChanged(() => { + updateProblemMatchers(); + jsonRegistry.notifySchemaChanged(tasksSchemaId); + })); + + this._register(TaskDefinitionRegistry.onDefinitionsChanged(() => { + updateTaskDefinitions(); + jsonRegistry.notifySchemaChanged(tasksSchemaId); + })); + } +} +registerWorkbenchContribution2(TaskRegistryContribution.ID, TaskRegistryContribution, WorkbenchPhase.AfterRestored); -TaskDefinitionRegistry.onDefinitionsChanged(() => { - updateTaskDefinitions(); - jsonRegistry.notifySchemaChanged(tasksSchemaId); -}); const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ diff --git a/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts b/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts index 6b41d88c5f7..eba74599af7 100644 --- a/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as matchers from 'vs/workbench/contrib/tasks/common/problemMatcher'; -import * as assert from 'assert'; +import assert from 'assert'; import { ValidationState, IProblemReporter, ValidationStatus } from 'vs/base/common/parsers'; class ProblemReporter implements IProblemReporter { diff --git a/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts index 816894f6b32..7aad1e55307 100644 --- a/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import * as assert from 'assert'; +import assert from 'assert'; import Severity from 'vs/base/common/severity'; import * as UUID from 'vs/base/common/uuid'; diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index c397b896093..65b2301495f 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -31,7 +31,7 @@ import { mainWindow } from 'vs/base/browser/window'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { isBoolean, isNumber, isString } from 'vs/base/common/types'; import { LayoutSettings } from 'vs/workbench/services/layout/browser/layoutService'; -import { AutoUpdateConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; +import { AutoRestartConfigurationKey, AutoUpdateConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; import { KEYWORD_ACTIVIATION_SETTING_ID } from 'vs/workbench/contrib/chat/common/chatService'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; @@ -431,13 +431,22 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc ? 'default' : 'custom'; this.telemetryService.publicLog2('window.systemColorTheme', { settingValue, source }); + }>('window.newWindowProfile', { settingValue, source }); return; } + + case AutoRestartConfigurationKey: + this.telemetryService.publicLog2('extensions.autoRestart', { settingValue: this.getValueToReport(key, target), source }); + return; } } diff --git a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts index e355b287f63..606202e8837 100644 --- a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts +++ b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts @@ -39,7 +39,7 @@ export class EnvironmentVariableInfoStale implements IEnvironmentVariableInfo { private _getActions(): ITerminalStatusHoverAction[] { return [{ - label: localize('relaunchTerminalLabel', "Relaunch terminal"), + label: localize('relaunchTerminalLabel', "Relaunch Terminal"), run: () => this._terminalService.getInstanceFromId(this._terminalId)?.relaunch(), commandId: TerminalCommandId.Relaunch }]; @@ -77,7 +77,7 @@ export class EnvironmentVariableInfoChangesActive implements IEnvironmentVariabl private _getActions(scope: EnvironmentVariableScope | undefined): ITerminalStatusHoverAction[] { return [{ - label: localize('showEnvironmentContributions', "Show environment contributions"), + label: localize('showEnvironmentContributions', "Show Environment Contributions"), run: () => this._commandService.executeCommand(TerminalCommandId.ShowEnvironmentContributions, scope), commandId: TerminalCommandId.ShowEnvironmentContributions }]; diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index aad118a4608..06036fcbaae 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -112,18 +112,26 @@ __vsc_escape_value() { fi # Process text byte by byte, not by codepoint. - builtin local LC_ALL=C str="${1}" i byte token out='' + local -r LC_ALL=C + local -r str="${1}" + local -ir len="${#str}" + + local -i i + local -i val + local byte + local token + local out='' for (( i=0; i < "${#str}"; ++i )); do + # Escape backslashes, semi-colons specially, then special ASCII chars below space (0x20). byte="${str:$i:1}" - - # Escape backslashes, semi-colons specially, then special ASCII chars below space (0x20) - if [ "$byte" = "\\" ]; then + builtin printf -v val '%d' "'$byte" + if (( val < 31 )); then + builtin printf -v token '\\x%02x' "'$byte" + elif (( val == 92 )); then # \ token="\\\\" - elif [ "$byte" = ";" ]; then + elif (( val == 59 )); then # ; token="\\x3b" - elif (( $(builtin printf '%d' "'$byte") < 31 )); then - token=$(builtin printf '\\x%02x' "'$byte") else token="$byte" fi @@ -131,7 +139,7 @@ __vsc_escape_value() { out+="$token" done - builtin printf '%s\n' "${out}" + builtin printf '%s\n' "$out" } # Send the IsWindows property if the environment looks like Windows @@ -162,8 +170,14 @@ __vsc_current_command="" __vsc_nonce="$VSCODE_NONCE" unset VSCODE_NONCE +# Some features should only work in Insiders +__vsc_stable="$VSCODE_STABLE" +unset VSCODE_STABLE + # Report continuation prompt -builtin printf "\e]633;P;ContinuationPrompt=$(echo "$PS2" | sed 's/\x1b/\\\\x1b/g')\a" +if [ "$__vsc_stable" = "0" ]; then + builtin printf "\e]633;P;ContinuationPrompt=$(echo "$PS2" | sed 's/\x1b/\\\\x1b/g')\a" +fi __vsc_report_prompt() { # Expand the original PS1 similarly to how bash would normally @@ -244,7 +258,10 @@ __vsc_update_prompt() { __vsc_precmd() { __vsc_command_complete "$__vsc_status" __vsc_current_command="" - __vsc_report_prompt + # Report prompt is a work in progress, currently encoding is too slow + if [ "$__vsc_stable" = "0" ]; then + __vsc_report_prompt + fi __vsc_first_prompt=1 __vsc_update_prompt } diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 index c2971c0de2a..6c92130ec95 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 @@ -21,6 +21,9 @@ $Global:__LastHistoryId = -1 $Nonce = $env:VSCODE_NONCE $env:VSCODE_NONCE = $null +$isStable = $env:VSCODE_STABLE +$env:VSCODE_STABLE = $null + $osVersion = [System.Environment]::OSVersion.Version $isWindows10 = $IsWindows -and $osVersion.Major -eq 10 -and $osVersion.Minor -eq 0 -and $osVersion.Build -lt 22000 @@ -52,7 +55,7 @@ if ($env:VSCODE_ENV_APPEND) { function Global:__VSCode-Escape-Value([string]$value) { # NOTE: In PowerShell v6.1+, this can be written `$value -replace '…', { … }` instead of `[regex]::Replace`. # Replace any non-alphanumeric characters. - [regex]::Replace($value, "[$([char]0x1b)\\\n;]", { param($match) + [regex]::Replace($value, "[$([char]0x00)-$([char]0x1f)\\\n;]", { param($match) # Encode the (ascii) matches as `\x` -Join ( [System.Text.Encoding]::UTF8.GetBytes($match.Value) | ForEach-Object { '\x{0:x2}' -f $_ } @@ -95,7 +98,9 @@ function Global:Prompt() { # Prompt # OSC 633 ; = ST - $Result += "$([char]0x1b)]633;P;Prompt=$(__VSCode-Escape-Value $OriginalPrompt)`a" + if ($isStable -eq "0") { + $Result += "$([char]0x1b)]633;P;Prompt=$(__VSCode-Escape-Value $OriginalPrompt)`a" + } # Write command started $Result += "$([char]0x1b)]633;B`a" @@ -142,9 +147,11 @@ else { } # Set ContinuationPrompt property -$ContinuationPrompt = (Get-PSReadLineOption).ContinuationPrompt -if ($ContinuationPrompt) { - [Console]::Write("$([char]0x1b)]633;P;ContinuationPrompt=$(__VSCode-Escape-Value $ContinuationPrompt)`a") +if ($isStable -eq "0") { + $ContinuationPrompt = (Get-PSReadLineOption).ContinuationPrompt + if ($ContinuationPrompt) { + [Console]::Write("$([char]0x1b)]633;P;ContinuationPrompt=$(__VSCode-Escape-Value $ContinuationPrompt)`a") + } } # Set always on key handlers which map to default VS Code keybindings @@ -169,8 +176,9 @@ function Set-MappedKeyHandlers { Set-MappedKeyHandler -Chord Shift+Enter -Sequence 'F12,c' Set-MappedKeyHandler -Chord Shift+End -Sequence 'F12,d' - # Conditionally enable suggestions - if ($env:VSCODE_SUGGEST -eq '1') { + # Enable suggestions if the environment variable is set and Windows PowerShell is not being used + # as APIs are not available to support this feature + if ($env:VSCODE_SUGGEST -eq '1' -and $PSVersionTable.PSVersion -ge "6.0") { Remove-Item Env:VSCODE_SUGGEST # VS Code send completions request (may override Ctrl+Spacebar) @@ -207,7 +215,19 @@ function Send-Completions { $completions = TabExpansion2 -inputScript $completionPrefix -cursorColumn $cursorIndex if ($null -ne $completions.CompletionMatches) { $result += ";$($completions.ReplacementIndex);$($completions.ReplacementLength);$($cursorIndex);" - $result += $completions.CompletionMatches | ConvertTo-Json -Compress + if ($completions.CompletionMatches.Count -gt 0 -and $completions.CompletionMatches.Where({ $_.ResultType -eq 3 -or $_.ResultType -eq 4 })) { + $json = [System.Collections.ArrayList]@($completions.CompletionMatches) + # Add . and .. to the completions list + $json.Add([System.Management.Automation.CompletionResult]::new( + '.', '.', [System.Management.Automation.CompletionResultType]::ProviderContainer, (Get-Location).Path) + ) + $json.Add([System.Management.Automation.CompletionResult]::new( + '..', '..', [System.Management.Automation.CompletionResultType]::ProviderContainer, (Split-Path (Get-Location) -Parent)) + ) + $result += $json | ConvertTo-Json -Compress + } else { + $result += $completions.CompletionMatches | ConvertTo-Json -Compress + } } } # If there is no space, get completions using CompletionCompleters as it gives us more diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 9fb62580059..946f01880fb 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -213,7 +213,7 @@ registerSendSequenceKeybinding('\x1b[24~d', { // F12,d -> shift+end (SelectLine) mac: { primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.RightArrow } }); registerSendSequenceKeybinding('\x1b[24~e', { // F12,e -> ctrl+space (Native suggest) - when: ContextKeyExpr.and(TerminalContextKeys.focus, ContextKeyExpr.equals(TerminalContextKeyStrings.ShellType, WindowsShellType.PowerShell), TerminalContextKeys.terminalShellIntegrationEnabled, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate(), ContextKeyExpr.or(ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true), ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.EnabledLegacy}`, true))), + when: ContextKeyExpr.and(TerminalContextKeys.focus, ContextKeyExpr.equals(TerminalContextKeyStrings.ShellType, WindowsShellType.PowerShell), TerminalContextKeys.terminalShellIntegrationEnabled, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate(), ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true)), primary: KeyMod.CtrlCmd | KeyCode.Space, mac: { primary: KeyMod.WinCtrl | KeyCode.Space } }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index 466958b0319..57848596b73 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -177,7 +177,7 @@ class SplitPaneContainer extends Disposable { // Remove old split view while (this._container.children.length > 0) { - this._container.removeChild(this._container.children[0]); + this._container.children[0].remove(); } this._splitViewDisposables.clear(); this._splitView.dispose(); @@ -288,7 +288,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { this._onPanelOrientationChanged.fire(this._terminalLocation === ViewContainerLocation.Panel && this._panelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL); this._register(toDisposable(() => { if (this._container && this._groupElement) { - this._container.removeChild(this._groupElement); + this._groupElement.remove(); this._groupElement = undefined; } })); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index e6ca453010d..a48841d9802 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -88,6 +88,7 @@ import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/commo import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { shouldPasteTerminalText } from 'vs/workbench/contrib/terminal/common/terminalClipboard'; import { TerminalIconPicker } from 'vs/workbench/contrib/terminal/browser/terminalIconPicker'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; // HACK: This file should not depend on terminalContrib // eslint-disable-next-line local/code-import-patterns @@ -702,7 +703,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const verticalPadding = parseInt(computedStyle.paddingTop) + parseInt(computedStyle.paddingBottom); TerminalInstance._lastKnownCanvasDimensions = new dom.Dimension( Math.min(Constants.MaxCanvasWidth, width - horizontalPadding), - height + (this._hasScrollBar && !this._horizontalScrollbar ? -5/* scroll bar height */ : 0) - 2/* bottom padding */ - verticalPadding); + height - verticalPadding + (this._hasScrollBar && this._horizontalScrollbar ? -5/* scroll bar height */ : 0)); return TerminalInstance._lastKnownCanvasDimensions; } @@ -857,7 +858,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Determine whether to send ETX (ctrl+c) before running the command. This should always // happen unless command detection can reliably say that a command is being entered and // there is no content in the prompt - if (commandDetection?.hasInput !== false) { + if (!commandDetection || commandDetection.promptInputModel.value.length > 0) { await this.sendText('\x03', false); // Wait a little before running the command to avoid the sequences being echoed while the ^C // is being evaluated @@ -1843,60 +1844,74 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } - @debounce(50) - private async _resize(): Promise { - this._resizeNow(false); - } + private async _resize(immediate?: boolean): Promise { + if (!this.xterm) { + return; + } - private async _resizeNow(immediate: boolean): Promise { let cols = this.cols; let rows = this.rows; - if (this.xterm) { - // Only apply these settings when the terminal is visible so that - // the characters are measured correctly. - if (this._isVisible && this._layoutSettingsChanged) { - const font = this.xterm.getFont(); - const config = this._terminalConfigurationService.config; - this.xterm.raw.options.letterSpacing = font.letterSpacing; - this.xterm.raw.options.lineHeight = font.lineHeight; - this.xterm.raw.options.fontSize = font.fontSize; - this.xterm.raw.options.fontFamily = font.fontFamily; - this.xterm.raw.options.fontWeight = config.fontWeight; - this.xterm.raw.options.fontWeightBold = config.fontWeightBold; - - // Any of the above setting changes could have changed the dimensions of the - // terminal, re-evaluate now. - this._initDimensions(); - cols = this.cols; - rows = this.rows; - - this._layoutSettingsChanged = false; - } - - if (isNaN(cols) || isNaN(rows)) { - return; - } + // Only apply these settings when the terminal is visible so that + // the characters are measured correctly. + if (this._isVisible && this._layoutSettingsChanged) { + const font = this.xterm.getFont(); + const config = this._terminalConfigurationService.config; + this.xterm.raw.options.letterSpacing = font.letterSpacing; + this.xterm.raw.options.lineHeight = font.lineHeight; + this.xterm.raw.options.fontSize = font.fontSize; + this.xterm.raw.options.fontFamily = font.fontFamily; + this.xterm.raw.options.fontWeight = config.fontWeight; + this.xterm.raw.options.fontWeightBold = config.fontWeightBold; + + // Any of the above setting changes could have changed the dimensions of the + // terminal, re-evaluate now. + this._initDimensions(); + cols = this.cols; + rows = this.rows; - if (cols !== this.xterm.raw.cols || rows !== this.xterm.raw.rows) { - if (this._fixedRows || this._fixedCols) { - await this._updateProperty(ProcessPropertyType.FixedDimensions, { cols: this._fixedCols, rows: this._fixedRows }); - } - this._onDimensionsChanged.fire(); - } + this._layoutSettingsChanged = false; + } - this.xterm.raw.resize(cols, rows); - TerminalInstance._lastKnownGridDimensions = { cols, rows }; + if (isNaN(cols) || isNaN(rows)) { + return; } + if (cols !== this.xterm.raw.cols || rows !== this.xterm.raw.rows) { + if (this._fixedRows || this._fixedCols) { + await this._updateProperty(ProcessPropertyType.FixedDimensions, { cols: this._fixedCols, rows: this._fixedRows }); + } + this._onDimensionsChanged.fire(); + } + + TerminalInstance._lastKnownGridDimensions = { cols, rows }; + if (immediate) { - // do not await, call setDimensions synchronously - this._processManager.setDimensions(cols, rows, true); + this.xterm.raw.resize(cols, rows); + await this._updatePtyDimensions(this.xterm.raw); } else { - await this._processManager.setDimensions(cols, rows); + // Update dimensions independently as vertical resize is cheap but horizontal resize is + // expensive due to reflow. + this._resizeVertically(this.xterm.raw, rows); + this._resizeHorizontally(this.xterm.raw, cols); } } + private async _resizeVertically(rawXterm: XTermTerminal, rows: number): Promise { + rawXterm.resize(rawXterm.cols, rows); + await this._updatePtyDimensions(rawXterm); + } + + @debounce(50) + private async _resizeHorizontally(rawXterm: XTermTerminal, cols: number): Promise { + rawXterm.resize(cols, rawXterm.rows); + await this._updatePtyDimensions(rawXterm); + } + + private async _updatePtyDimensions(rawXterm: XTermTerminal): Promise { + await this._processManager.setDimensions(rawXterm.cols, rawXterm.rows); + } + setShellType(shellType: TerminalShellType | undefined) { if (this._shellType === shellType) { return; @@ -1976,7 +1991,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } this._dimensionsOverride = dimensions; if (immediate) { - this._resizeNow(true); + this._resize(true); } else { this._resize(); } @@ -2288,15 +2303,14 @@ class TerminalInstanceDragAndDropController extends Disposable implements dom.ID private readonly _container: HTMLElement, @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, + @IHostService private readonly _hostService: IHostService, ) { super(); this._register(toDisposable(() => this._clearDropOverlay())); } private _clearDropOverlay() { - if (this._dropOverlay && this._dropOverlay.parentElement) { - this._dropOverlay.parentElement.removeChild(this._dropOverlay); - } + this._dropOverlay?.remove(); this._dropOverlay = undefined; } @@ -2372,9 +2386,9 @@ class TerminalInstanceDragAndDropController extends Disposable implements dom.ID path = URI.file(JSON.parse(rawCodeFiles)[0]); } - if (!path && e.dataTransfer.files.length > 0 && e.dataTransfer.files[0].path /* Electron only */) { + if (!path && e.dataTransfer.files.length > 0 && this._hostService.getPathForFile(e.dataTransfer.files[0])) { // Check if the file was dragged from the filesystem - path = URI.file(e.dataTransfer.files[0].path); + path = URI.file(this._hostService.getPathForFile(e.dataTransfer.files[0])!); } if (!path) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 01db326f019..22204b7a88e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -289,7 +289,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const options: ITerminalProcessOptions = { shellIntegration: { enabled: this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled), - suggestEnabled: this._configurationService.getValue(TerminalSuggestSettingId.Enabled) || this._configurationService.getValue(TerminalSuggestSettingId.EnabledLegacy), + suggestEnabled: this._configurationService.getValue(TerminalSuggestSettingId.Enabled), nonce: this.shellIntegrationNonce }, windowsEnableConpty: this._terminalConfigurationService.config.windowsEnableConpty, @@ -489,7 +489,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const options: ITerminalProcessOptions = { shellIntegration: { enabled: this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled), - suggestEnabled: this._configurationService.getValue(TerminalSuggestSettingId.Enabled) || this._configurationService.getValue(TerminalSuggestSettingId.EnabledLegacy), + suggestEnabled: this._configurationService.getValue(TerminalSuggestSettingId.Enabled), nonce: this.shellIntegrationNonce }, windowsEnableConpty: this._terminalConfigurationService.config.windowsEnableConpty, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts index 24dde378f30..a4b662a065c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -22,6 +22,7 @@ import { URI } from 'vs/base/common/uri'; import { deepClone } from 'vs/base/common/objects'; import { isUriComponents } from 'vs/platform/terminal/common/terminalProfiles'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { Disposable } from 'vs/base/common/lifecycle'; export interface IProfileContextProvider { getDefaultSystemShell(remoteAuthority: string | undefined, os: OperatingSystem): Promise; @@ -34,7 +35,7 @@ const generatedProfileName = 'Generated'; * Resolves terminal shell launch config and terminal profiles for the given operating system, * environment, and user configuration. */ -export abstract class BaseTerminalProfileResolverService implements ITerminalProfileResolverService { +export abstract class BaseTerminalProfileResolverService extends Disposable implements ITerminalProfileResolverService { declare _serviceBrand: undefined; private _primaryBackendOs: OperatingSystem | undefined; @@ -54,19 +55,21 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro private readonly _workspaceContextService: IWorkspaceContextService, private readonly _remoteAgentService: IRemoteAgentService ) { + super(); + if (this._remoteAgentService.getConnection()) { this._remoteAgentService.getEnvironment().then(env => this._primaryBackendOs = env?.os || OS); } else { this._primaryBackendOs = OS; } - this._configurationService.onDidChangeConfiguration(e => { + this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(TerminalSettingId.DefaultProfileWindows) || e.affectsConfiguration(TerminalSettingId.DefaultProfileMacOs) || e.affectsConfiguration(TerminalSettingId.DefaultProfileLinux)) { this._refreshDefaultProfileName(); } - }); - this._terminalProfileService.onDidChangeAvailableProfiles(() => this._refreshDefaultProfileName()); + })); + this._register(this._terminalProfileService.onDidChangeAvailableProfiles(() => this._refreshDefaultProfileName())); } @debounce(200) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts index eb3dc1c08f3..ce5bf14c23c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts @@ -71,7 +71,7 @@ export class TerminalProfileService extends Disposable implements ITerminalProfi // in web, we don't want to show the dropdown unless there's a web extension // that contributes a profile - this._extensionService.onDidChangeExtensions(() => this.refreshAvailableProfiles()); + this._register(this._extensionService.onDidChangeExtensions(() => this.refreshAvailableProfiles())); this._webExtensionContributedProfileContextKey = TerminalContextKeys.webExtensionContributedProfile.bindTo(this._contextKeyService); this._updateWebContextKey(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts b/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts index a17b660a13c..b795f91f201 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts @@ -269,6 +269,9 @@ export async function showRunRecentQuickPick( return; } const [item] = quickPick.activeItems; + if (!item) { + return; + } if ('command' in item && item.command && item.command.marker) { if (!terminalScrollStateSaved) { xterm.markTracker.saveScrollState(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 1993319a1e7..2973275e7c8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -294,24 +294,26 @@ export class TerminalService extends Disposable implements ITerminalService { const isPersistentRemote = !!this._environmentService.remoteAuthority && enableTerminalReconnection; - this._primaryBackend?.onDidRequestDetach(async (e) => { - const instanceToDetach = this.getInstanceFromResource(getTerminalUri(e.workspaceId, e.instanceId)); - if (instanceToDetach) { - const persistentProcessId = instanceToDetach?.persistentProcessId; - if (persistentProcessId && !instanceToDetach.shellLaunchConfig.isFeatureTerminal && !instanceToDetach.shellLaunchConfig.customPtyImplementation) { - if (instanceToDetach.target === TerminalLocation.Editor) { - this._terminalEditorService.detachInstance(instanceToDetach); + if (this._primaryBackend) { + this._register(this._primaryBackend.onDidRequestDetach(async (e) => { + const instanceToDetach = this.getInstanceFromResource(getTerminalUri(e.workspaceId, e.instanceId)); + if (instanceToDetach) { + const persistentProcessId = instanceToDetach?.persistentProcessId; + if (persistentProcessId && !instanceToDetach.shellLaunchConfig.isFeatureTerminal && !instanceToDetach.shellLaunchConfig.customPtyImplementation) { + if (instanceToDetach.target === TerminalLocation.Editor) { + this._terminalEditorService.detachInstance(instanceToDetach); + } else { + this._terminalGroupService.getGroupForInstance(instanceToDetach)?.removeInstance(instanceToDetach); + } + await instanceToDetach.detachProcessAndDispose(TerminalExitReason.User); + await this._primaryBackend?.acceptDetachInstanceReply(e.requestId, persistentProcessId); } else { - this._terminalGroupService.getGroupForInstance(instanceToDetach)?.removeInstance(instanceToDetach); + // will get rejected without a persistentProcessId to attach to + await this._primaryBackend?.acceptDetachInstanceReply(e.requestId, undefined); } - await instanceToDetach.detachProcessAndDispose(TerminalExitReason.User); - await this._primaryBackend?.acceptDetachInstanceReply(e.requestId, persistentProcessId); - } else { - // will get rejected without a persistentProcessId to attach to - await this._primaryBackend?.acceptDetachInstanceReply(e.requestId, undefined); } - } - }); + })); + } mark('code/terminal/willReconnect'); let reconnectedPromise: Promise; @@ -344,16 +346,16 @@ export class TerminalService extends Disposable implements ITerminalService { } private _forwardInstanceHostEvents(host: ITerminalInstanceHost) { - host.onDidChangeInstances(this._onDidChangeInstances.fire, this._onDidChangeInstances); - host.onDidDisposeInstance(this._onDidDisposeInstance.fire, this._onDidDisposeInstance); - host.onDidChangeActiveInstance(instance => this._evaluateActiveInstance(host, instance)); - host.onDidFocusInstance(instance => { + this._register(host.onDidChangeInstances(this._onDidChangeInstances.fire, this._onDidChangeInstances)); + this._register(host.onDidDisposeInstance(this._onDidDisposeInstance.fire, this._onDidDisposeInstance)); + this._register(host.onDidChangeActiveInstance(instance => this._evaluateActiveInstance(host, instance))); + this._register(host.onDidFocusInstance(instance => { this._onDidFocusInstance.fire(instance); this._evaluateActiveInstance(host, instance); - }); - host.onDidChangeInstanceCapability((instance) => { + })); + this._register(host.onDidChangeInstanceCapability((instance) => { this._onDidChangeInstanceCapability.fire(instance); - }); + })); this._hostActiveTerminals.set(host, undefined); } @@ -1240,7 +1242,7 @@ class TerminalEditorStyle extends Themable { super(_themeService); this._registerListeners(); this._styleElement = dom.createStyleSheet(container); - this._register(toDisposable(() => container.removeChild(this._styleElement))); + this._register(toDisposable(() => this._styleElement.remove())); this.updateStyles(); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index a93fccec1b5..285c7a72ec1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -174,9 +174,7 @@ export class TerminalTabbedView extends Disposable { } else { if (this._splitView.length === 2 && !this._terminalTabsMouseContextKey.get()) { this._splitView.removeView(this._tabTreeIndex); - if (this._plusButton) { - this._tabContainer.removeChild(this._plusButton); - } + this._plusButton?.remove(); this._removeSashListener(); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index b32bd3f28d6..b81f20a1e7d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -51,6 +51,7 @@ import { Schemas } from 'vs/base/common/network'; import { getColorForSeverity } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; import { TerminalContextActionRunner } from 'vs/workbench/contrib/terminal/browser/terminalContextMenu'; import type { IHoverAction } from 'vs/base/browser/ui/hover/hover'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; const $ = DOM.$; @@ -577,6 +578,7 @@ class TerminalTabsDragAndDrop extends Disposable implements IListDragAndDrop 0 && e.dataTransfer.files[0].path /* Electron only */) { + if (!resource && e.dataTransfer.files.length > 0 && this._hostService.getPathForFile(e.dataTransfer.files[0])) { // Check if the file was dragged from the filesystem - resource = URI.file(e.dataTransfer.files[0].path); + resource = URI.file(this._hostService.getPathForFile(e.dataTransfer.files[0])!); } if (!resource) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 966c4ead234..3cf3c286a93 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -585,7 +585,7 @@ class TerminalThemeIconStyle extends Themable { super(_themeService); this._registerListeners(); this._styleElement = dom.createStyleSheet(container); - this._register(toDisposable(() => container.removeChild(this._styleElement))); + this._register(toDisposable(() => this._styleElement.remove())); this.updateStyles(); } diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts b/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts index 032610dbea7..63283a6a76b 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts @@ -19,8 +19,8 @@ export class TerminalWidgetManager implements IDisposable { } dispose(): void { - if (this._container && this._container.parentElement) { - this._container.parentElement.removeChild(this._container); + if (this._container) { + this._container.remove(); this._container = undefined; } } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 67136657ac5..8d14cc3a245 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -9,6 +9,7 @@ import type { Unicode11Addon as Unicode11AddonType } from '@xterm/addon-unicode1 import type { WebglAddon as WebglAddonType } from '@xterm/addon-webgl'; import type { SerializeAddon as SerializeAddonType } from '@xterm/addon-serialize'; import type { ImageAddon as ImageAddonType } from '@xterm/addon-image'; +import type { ClipboardAddon as ClipboardAddonType, ClipboardSelectionType } from '@xterm/addon-clipboard'; import * as dom from 'vs/base/browser/dom'; import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -44,6 +45,7 @@ const enum RenderConstants { SmoothScrollDuration = 125 } +let ClipboardAddon: typeof ClipboardAddonType; let ImageAddon: typeof ImageAddonType; let SearchAddon: typeof SearchAddonType; let SerializeAddon: typeof SerializeAddonType; @@ -118,6 +120,9 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach private _shellIntegrationAddon: ShellIntegrationAddon; private _decorationAddon: DecorationAddon; + // Always on dynamicly imported addons + private _clipboardAddon?: ClipboardAddonType; + // Optional addons private _searchAddon?: SearchAddonType; private _unicode11Addon?: Unicode11AddonType; @@ -273,6 +278,17 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach this.raw.loadAddon(this._decorationAddon); this._shellIntegrationAddon = new ShellIntegrationAddon(shellIntegrationNonce, disableShellIntegrationReporting, this._telemetryService, this._logService); this.raw.loadAddon(this._shellIntegrationAddon); + this._getClipboardAddonConstructor().then(ClipboardAddon => { + this._clipboardAddon = this._instantiationService.createInstance(ClipboardAddon, undefined, { + async readText(type: ClipboardSelectionType): Promise { + return _clipboardService.readText(type === 'p' ? 'selection' : 'clipboard'); + }, + async writeText(type: ClipboardSelectionType, text: string): Promise { + return _clipboardService.writeText(text, type === 'p' ? 'selection' : 'clipboard'); + } + }); + this.raw.loadAddon(this._clipboardAddon); + }); this._anyTerminalFocusContextKey = TerminalContextKeys.focusInAny.bindTo(contextKeyService); this._anyFocusedTerminalHasSelection = TerminalContextKeys.textSelectedInFocused.bindTo(contextKeyService); @@ -325,7 +341,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach this.raw.open(container); } - // TODO: Move before open to the DOM renderer doesn't initialize + // TODO: Move before open so the DOM renderer doesn't initialize if (options.enableGpu) { if (this._shouldLoadWebgl()) { this._enableWebglRenderer(); @@ -710,6 +726,13 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach } } + protected async _getClipboardAddonConstructor(): Promise { + if (!ClipboardAddon) { + ClipboardAddon = (await importAMDNodeModule('@xterm/addon-clipboard', 'lib/addon-clipboard.js')).ClipboardAddon; + } + return ClipboardAddon; + } + protected async _getImageAddonConstructor(): Promise { if (!ImageAddon) { ImageAddon = (await importAMDNodeModule('@xterm/addon-image', 'lib/addon-image.js')).ImageAddon; diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index fc4a6a0a0e0..81ccf23927f 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -209,8 +209,6 @@ export interface ITerminalConfiguration { shellIntegration?: { enabled: boolean; decorationsEnabled: boolean; - // TODO: Legacy - remove soon - suggestEnabled: boolean; }; enableImages: boolean; smoothScrolling: boolean; diff --git a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts index ced2afffa11..86a5a037b4d 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts @@ -23,12 +23,7 @@ export const TERMINAL_FOREGROUND_COLOR = registerColor('terminal.foreground', { }, nls.localize('terminal.foreground', 'The foreground color of the terminal.')); export const TERMINAL_CURSOR_FOREGROUND_COLOR = registerColor('terminalCursor.foreground', null, nls.localize('terminalCursor.foreground', 'The foreground color of the terminal cursor.')); export const TERMINAL_CURSOR_BACKGROUND_COLOR = registerColor('terminalCursor.background', null, nls.localize('terminalCursor.background', 'The background color of the terminal cursor. Allows customizing the color of a character overlapped by a block cursor.')); -export const TERMINAL_SELECTION_BACKGROUND_COLOR = registerColor('terminal.selectionBackground', { - light: editorSelectionBackground, - dark: editorSelectionBackground, - hcDark: editorSelectionBackground, - hcLight: editorSelectionBackground -}, nls.localize('terminal.selectionBackground', 'The selection background color of the terminal.')); +export const TERMINAL_SELECTION_BACKGROUND_COLOR = registerColor('terminal.selectionBackground', editorSelectionBackground, nls.localize('terminal.selectionBackground', 'The selection background color of the terminal.')); export const TERMINAL_INACTIVE_SELECTION_BACKGROUND_COLOR = registerColor('terminal.inactiveSelectionBackground', { light: transparent(TERMINAL_SELECTION_BACKGROUND_COLOR, 0.5), dark: transparent(TERMINAL_SELECTION_BACKGROUND_COLOR, 0.5), @@ -59,18 +54,8 @@ export const TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR = registerColor( hcDark: '#F14C4C', hcLight: '#B5200D' }, nls.localize('terminalCommandDecoration.errorBackground', 'The terminal command decoration background color for error commands.')); -export const TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR = registerColor('terminalOverviewRuler.cursorForeground', { - dark: '#A0A0A0CC', - light: '#A0A0A0CC', - hcDark: '#A0A0A0CC', - hcLight: '#A0A0A0CC' -}, nls.localize('terminalOverviewRuler.cursorForeground', 'The overview ruler cursor color.')); -export const TERMINAL_BORDER_COLOR = registerColor('terminal.border', { - dark: PANEL_BORDER, - light: PANEL_BORDER, - hcDark: PANEL_BORDER, - hcLight: PANEL_BORDER -}, nls.localize('terminal.border', 'The color of the border that separates split panes within the terminal. This defaults to panel.border.')); +export const TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR = registerColor('terminalOverviewRuler.cursorForeground', '#A0A0A0CC', nls.localize('terminalOverviewRuler.cursorForeground', 'The overview ruler cursor color.')); +export const TERMINAL_BORDER_COLOR = registerColor('terminal.border', PANEL_BORDER, nls.localize('terminal.border', 'The color of the border that separates split panes within the terminal. This defaults to panel.border.')); export const TERMINAL_FIND_MATCH_BACKGROUND_COLOR = registerColor('terminal.findMatchBackground', { dark: editorFindMatch, light: editorFindMatch, @@ -78,12 +63,7 @@ export const TERMINAL_FIND_MATCH_BACKGROUND_COLOR = registerColor('terminal.find hcDark: null, hcLight: '#0F4A85' }, nls.localize('terminal.findMatchBackground', 'Color of the current search match in the terminal. The color must not be opaque so as not to hide underlying terminal content.'), true); -export const TERMINAL_HOVER_HIGHLIGHT_BACKGROUND_COLOR = registerColor('terminal.hoverHighlightBackground', { - dark: transparent(editorHoverHighlight, 0.5), - light: transparent(editorHoverHighlight, 0.5), - hcDark: transparent(editorHoverHighlight, 0.5), - hcLight: transparent(editorHoverHighlight, 0.5) -}, nls.localize('terminal.findMatchHighlightBorder', 'Border color of the other search matches in the terminal.')); +export const TERMINAL_HOVER_HIGHLIGHT_BACKGROUND_COLOR = registerColor('terminal.hoverHighlightBackground', transparent(editorHoverHighlight, 0.5), nls.localize('terminal.findMatchHighlightBorder', 'Border color of the other search matches in the terminal.')); export const TERMINAL_FIND_MATCH_BORDER_COLOR = registerColor('terminal.findMatchBorder', { dark: null, light: null, @@ -108,18 +88,14 @@ export const TERMINAL_OVERVIEW_RULER_FIND_MATCH_FOREGROUND_COLOR = registerColor hcDark: '#f38518', hcLight: '#0F4A85' }, nls.localize('terminalOverviewRuler.findMatchHighlightForeground', 'Overview ruler marker color for find matches in the terminal.')); -export const TERMINAL_DRAG_AND_DROP_BACKGROUND = registerColor('terminal.dropBackground', { - dark: EDITOR_DRAG_AND_DROP_BACKGROUND, - light: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcDark: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcLight: EDITOR_DRAG_AND_DROP_BACKGROUND -}, nls.localize('terminal.dragAndDropBackground', "Background color when dragging on top of terminals. The color should have transparency so that the terminal contents can still shine through."), true); -export const TERMINAL_TAB_ACTIVE_BORDER = registerColor('terminal.tab.activeBorder', { - dark: TAB_ACTIVE_BORDER, - light: TAB_ACTIVE_BORDER, - hcDark: TAB_ACTIVE_BORDER, - hcLight: TAB_ACTIVE_BORDER -}, nls.localize('terminal.tab.activeBorder', 'Border on the side of the terminal tab in the panel. This defaults to tab.activeBorder.')); +export const TERMINAL_DRAG_AND_DROP_BACKGROUND = registerColor('terminal.dropBackground', EDITOR_DRAG_AND_DROP_BACKGROUND, nls.localize('terminal.dragAndDropBackground', "Background color when dragging on top of terminals. The color should have transparency so that the terminal contents can still shine through."), true); +export const TERMINAL_TAB_ACTIVE_BORDER = registerColor('terminal.tab.activeBorder', TAB_ACTIVE_BORDER, nls.localize('terminal.tab.activeBorder', 'Border on the side of the terminal tab in the panel. This defaults to tab.activeBorder.')); +export const TERMINAL_INITIAL_HINT_FOREGROUND = registerColor('terminal.initialHintForeground', { + dark: '#ffffff56', + light: '#0007', + hcDark: null, + hcLight: null +}, nls.localize('terminalInitialHintForeground', 'Foreground color of the terminal initial hint.')); export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefaults } } = { 'terminal.ansiBlack': { diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts index 5ea55926c39..a15a395d9d8 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Extensions as ThemeingExtensions, IColorRegistry, ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { ansiColorIdentifiers, registerColors } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts index dd74c586060..5aa97b16452 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts index ef6c5b46e76..a547d3f68ae 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { importAMDNodeModule } from 'vs/amdX'; import { isWindows } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalInitialHint.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalInitialHint.css index 6055da8a2a9..c2f0de22f7d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalInitialHint.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalInitialHint.css @@ -4,11 +4,10 @@ *--------------------------------------------------------------------------------------------*/ .monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint { - color: var(--vscode-input-placeholderForeground); + color: var(--vscode-terminal-initialHintForeground); } .monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint a { cursor: pointer; - color: var(--vscode-textLink-foreground); } .monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint a, diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts index a2c974a0baa..ba1325e8af4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts @@ -18,6 +18,8 @@ registerTerminalContribution(TerminalChatController.ID, TerminalChatController, AccessibleViewRegistry.register(new TerminalInlineChatAccessibleView()); AccessibleViewRegistry.register(new TerminalChatAccessibilityHelp()); +registerWorkbenchContribution2(TerminalChatEnabler.Id, TerminalChatEnabler, WorkbenchPhase.AfterRestored); + // #endregion // #region Actions @@ -25,5 +27,7 @@ AccessibleViewRegistry.register(new TerminalChatAccessibilityHelp()); import 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { TerminalChatAccessibilityHelp } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; +import { registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions'; +import { TerminalChatEnabler } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler'; // #endregion diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts index a8f77d089e0..21d5f887145 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts @@ -29,9 +29,16 @@ import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal import 'vs/css!./media/terminalInitialHint'; import { TerminalInitialHintSettingId } from 'vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration'; import { ChatAgentLocation, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; const $ = dom.$; +const enum Constants { + InitialHintHideStorageKey = 'terminal.initialHint.hide' +} + export class InitialHintAddon extends Disposable implements ITerminalAddon { private readonly _onDidRequestCreateHint = this._register(new Emitter()); get onDidRequestCreateHint(): Event { return this._onDidRequestCreateHint.event; } @@ -90,11 +97,22 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @IStorageService private readonly _storageService: IStorageService, ) { super(); + + // Reset hint state when config changes + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(TerminalInitialHintSettingId.Enabled)) { + this._storageService.remove(Constants.InitialHintHideStorageKey, StorageScope.APPLICATION); + } + })); } xtermOpen(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { + if (this._storageService.getBoolean(Constants.InitialHintHideStorageKey, StorageScope.APPLICATION, false)) { + return; + } if (this._terminalGroupService.instances.length + this._terminalEditorService.instances.length !== 1) { // only show for the first terminal return; @@ -108,7 +126,7 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm private _createHint(): void { const instance = this._instance instanceof TerminalInstance ? this._instance : undefined; const commandDetectionCapability = instance?.capabilities.get(TerminalCapability.CommandDetection); - if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || commandDetectionCapability.promptInputModel.value || instance.reconnectionProperties) { + if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || commandDetectionCapability.promptInputModel.value || !!instance.shellLaunchConfig.attachPersistentProcess) { return; } @@ -130,19 +148,24 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm marker, x: this._xterm.raw.buffer.active.cursorX + 1, }); + if (this._decoration) { + this._register(this._decoration); + } } - this._register(this._xterm.raw.onKey(() => { - this._decoration?.dispose(); - this._addon?.dispose(); + this._register(this._xterm.raw.onKey(() => this.dispose())); + + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(TerminalInitialHintSettingId.Enabled) && !this._configurationService.getValue(TerminalInitialHintSettingId.Enabled)) { + this.dispose(); + } })); const inputModel = commandDetectionCapability.promptInputModel; if (inputModel) { this._register(inputModel.onDidChangeInput(() => { if (inputModel.value) { - this._decoration?.dispose(); - this._addon?.dispose(); + this.dispose(); } })); } @@ -181,8 +204,6 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm } registerTerminalContribution(TerminalInitialHintContribution.ID, TerminalInitialHintContribution, false); - - class TerminalInitialHintWidget extends Disposable { @@ -193,12 +214,15 @@ class TerminalInitialHintWidget extends Disposable { constructor( private readonly _instance: ITerminalInstance, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, @IKeybindingService private readonly keybindingService: IKeybindingService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IProductService private readonly productService: IProductService, - @ITerminalService private readonly terminalService: ITerminalService + @ITerminalService private readonly terminalService: ITerminalService, + @IStorageService private readonly _storageService: IStorageService, + @IContextMenuService private readonly contextMenuService: IContextMenuService ) { super(); this.toDispose.add(_instance.onDidFocus(() => { @@ -219,11 +243,16 @@ class TerminalInitialHintWidget extends Disposable { } private _getHintInlineChat(agents: IChatAgent[]) { - const providerName = (agents.length === 1 ? agents[0].fullName : undefined) ?? this.productService.nameShort; + let providerName = (agents.length === 1 ? agents[0].fullName : undefined) ?? this.productService.nameShort; + const defaultAgent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Panel); + if (defaultAgent?.extensionId.value === agents[0].extensionId.value) { + providerName = defaultAgent.fullName ?? providerName; + } let ariaLabel = `Ask ${providerName} something or start typing to dismiss.`; const handleClick = () => { + this._storageService.store(Constants.InitialHintHideStorageKey, true, StorageScope.APPLICATION, StorageTarget.USER); this.telemetryService.publicLog2('workbenchActionExecuted', { id: 'terminalInlineChat.hintAction', from: 'hint' @@ -232,6 +261,7 @@ class TerminalInitialHintWidget extends Disposable { }; this.toDispose.add(this.commandService.onDidExecuteCommand(e => { if (e.commandId === TerminalChatCommandId.Start) { + this._storageService.store(Constants.InitialHintHideStorageKey, true, StorageScope.APPLICATION, StorageTarget.USER); this.dispose(); } })); @@ -307,8 +337,23 @@ class TerminalInitialHintWidget extends Disposable { this.domNode = undefined; })); + this.toDispose.add(dom.addDisposableListener(this.domNode, dom.EventType.CONTEXT_MENU, (e) => { + this.contextMenuService.showContextMenu({ + getAnchor: () => { return new StandardMouseEvent(dom.getActiveWindow(), e); }, + getActions: () => { + return [{ + id: 'workench.action.disableTerminalInitialHint', + label: localize('disableInitialHint', "Disable Initial Hint"), + tooltip: localize('disableInitialHint', "Disable Initial Hint"), + enabled: true, + class: undefined, + run: () => this.configurationService.updateValue(TerminalInitialHintSettingId.Enabled, false) + } + ]; + } + }); + })); } - return this.domNode; } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index bf95499ee32..13d70f86352 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -15,9 +15,6 @@ export const enum TerminalChatCommandId { Discard = 'workbench.action.terminal.chat.discard', MakeRequest = 'workbench.action.terminal.chat.makeRequest', Cancel = 'workbench.action.terminal.chat.cancel', - FeedbackHelpful = 'workbench.action.terminal.chat.feedbackHelpful', - FeedbackUnhelpful = 'workbench.action.terminal.chat.feedbackUnhelpful', - FeedbackReportIssue = 'workbench.action.terminal.chat.feedbackReportIssue', RunCommand = 'workbench.action.terminal.chat.runCommand', RunFirstCommand = 'workbench.action.terminal.chat.runFirstCommand', InsertCommand = 'workbench.action.terminal.chat.insertCommand', @@ -30,7 +27,6 @@ export const enum TerminalChatCommandId { export const MENU_TERMINAL_CHAT_INPUT = MenuId.for('terminalChatInput'); export const MENU_TERMINAL_CHAT_WIDGET = MenuId.for('terminalChatWidget'); export const MENU_TERMINAL_CHAT_WIDGET_STATUS = MenuId.for('terminalChatWidget.status'); -export const MENU_TERMINAL_CHAT_WIDGET_FEEDBACK = MenuId.for('terminalChatWidget.feedback'); export const MENU_TERMINAL_CHAT_WIDGET_TOOLBAR = MenuId.for('terminalChatWidget.toolbar'); export const enum TerminalChatContextKeyStrings { @@ -61,18 +57,12 @@ export namespace TerminalChatContextKeys { /** Whether the chat input has text */ export const inputHasText = new RawContextKey(TerminalChatContextKeyStrings.ChatInputHasText, false, localize('chatInputHasTextContextKey', "Whether the chat input has text.")); - /** Whether the terminal chat agent has been registered */ - export const agentRegistered = new RawContextKey(TerminalChatContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether the terminal chat agent has been registered.")); - /** The chat response contains at least one code block */ export const responseContainsCodeBlock = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseContainsCodeBlock, false, localize('chatResponseContainsCodeBlockContextKey', "Whether the chat response contains a code block.")); /** The chat response contains multiple code blocks */ export const responseContainsMultipleCodeBlocks = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseContainsMultipleCodeBlocks, false, localize('chatResponseContainsMultipleCodeBlocksContextKey', "Whether the chat response contains multiple code blocks.")); - /** Whether the response supports issue reporting */ - export const responseSupportsIssueReporting = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseSupportsIssueReporting, false, localize('chatResponseSupportsIssueReportingContextKey', "Whether the response supports issue reporting")); - - /** The chat vote, if any for the response, if any */ - export const sessionResponseVote = new RawContextKey(TerminalChatContextKeyStrings.ChatSessionResponseVote, undefined, { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); + /** A chat agent exists for the terminal location */ + export const hasChatAgent = new RawContextKey(TerminalChatContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether a chat agent is registered for the terminal location.")); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 6b04dac4c54..d53b7cd4968 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -9,11 +9,11 @@ import { localize2 } from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { AbstractInlineChatAction } from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; -import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_AGENT } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; registerActiveXtermAction({ @@ -29,8 +29,7 @@ registerActiveXtermAction({ category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - // TODO: This needs to change to check for a terminal location capable agent - CTX_INLINE_CHAT_HAS_AGENT + TerminalChatContextKeys.hasChatAgent ), run: (_xterm, _accessor, activeInstance, opts?: unknown) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -164,7 +163,6 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.agentRegistered, TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.responseContainsMultipleCodeBlocks.negate() ), @@ -196,7 +194,6 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.agentRegistered, TerminalChatContextKeys.responseContainsMultipleCodeBlocks ), icon: Codicon.play, @@ -227,7 +224,6 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.agentRegistered, TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.responseContainsMultipleCodeBlocks.negate() ), @@ -259,7 +255,6 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.agentRegistered, TerminalChatContextKeys.responseContainsMultipleCodeBlocks ), keybinding: { @@ -289,14 +284,13 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.agentRegistered, ), icon: Codicon.commentDiscussion, menu: [{ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 1, - when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock.negate(), TerminalChatContextKeys.requestActive.negate()), + when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()), }, { id: MENU_TERMINAL_CHAT_WIDGET, @@ -319,12 +313,11 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.agentRegistered, CTX_INLINE_CHAT_EMPTY.negate() ), icon: Codicon.send, keybinding: { - when: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, TerminalChatContextKeys.requestActive.negate()), + when: ContextKeyExpr.and(TerminalChatContextKeys.focused, TerminalChatContextKeys.requestActive.negate()), weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Enter }, @@ -348,7 +341,6 @@ registerActiveXtermAction({ title: localize2('cancelChat', 'Cancel Chat'), precondition: ContextKeyExpr.and( TerminalChatContextKeys.requestActive, - TerminalChatContextKeys.agentRegistered ), icon: Codicon.debugStop, menu: { @@ -366,25 +358,39 @@ registerActiveXtermAction({ }); registerActiveXtermAction({ - id: TerminalChatCommandId.FeedbackReportIssue, - title: localize2('reportIssue', 'Report Issue'), - precondition: ContextKeyExpr.and( - TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), - TerminalChatContextKeys.responseSupportsIssueReporting - ), - icon: Codicon.report, - menu: [{ - id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, - when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), TerminalChatContextKeys.responseSupportsIssueReporting), - group: 'inline', - order: 3 - }], + id: TerminalChatCommandId.PreviousFromHistory, + title: localize2('previousFromHitory', 'Previous From History'), + precondition: TerminalChatContextKeys.focused, + keybinding: { + when: TerminalChatContextKeys.focused, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.UpArrow, + }, + + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.populateHistory(true); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.NextFromHistory, + title: localize2('nextFromHitory', 'Next From History'), + precondition: TerminalChatContextKeys.focused, + keybinding: { + when: TerminalChatContextKeys.focused, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.DownArrow, + }, + run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { return; } const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - contr?.acceptFeedback(); + contr?.populateHistory(false); } }); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 5734afb1b87..fb9ded6f870 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -4,26 +4,27 @@ *--------------------------------------------------------------------------------------------*/ import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; -import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { GeneratingPhrase, IChatAccessibilityService, IChatCodeBlockContextProviderService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatAgentLocation, IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatUserAction, IChatProgress, IChatService, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCodeBlockContextProviderService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { ChatModel, ChatRequestModel, IChatRequestVariableData, IChatResponseModel, getHistoryEntriesFromModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { DeferredPromise } from 'vs/base/common/async'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { assertType } from 'vs/base/common/types'; +import { CancelablePromise, createCancelablePromise, DeferredPromise } from 'vs/base/common/async'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; const enum Message { NONE = 0, @@ -48,6 +49,9 @@ export class TerminalChatController extends Disposable implements ITerminalContr */ static activeChatWidget?: TerminalChatController; + private static _storageKey = 'terminal-inline-chat-history'; + private static _promptHistory: string[] = []; + /** * The chat widget for the controller, this is lazy as we don't want to instantiate it until * both it's required and xterm is ready. @@ -61,17 +65,11 @@ export class TerminalChatController extends Disposable implements ITerminalContr get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } private readonly _requestActiveContextKey: IContextKey; - private readonly _terminalAgentRegisteredContextKey: IContextKey; private readonly _responseContainsCodeBlockContextKey: IContextKey; private readonly _responseContainsMulitpleCodeBlocksContextKey: IContextKey; - private readonly _responseSupportsIssueReportingContextKey: IContextKey; - private readonly _sessionResponseVoteContextKey: IContextKey; private _messages = this._store.add(new Emitter()); - private _currentRequest: ChatRequestModel | undefined; - - private _lastInput: string | undefined; private _lastResponseContent: string | undefined; get lastResponseContent(): string | undefined { return this._lastResponseContent; @@ -81,7 +79,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr get onDidHide() { return this.chatWidget?.onDidHide ?? Event.None; } private _terminalAgentName = 'terminal'; - private _terminalAgentId: string | undefined; private readonly _model: MutableDisposable = this._register(new MutableDisposable()); @@ -89,31 +86,32 @@ export class TerminalChatController extends Disposable implements ITerminalContr return this._chatWidget?.value.inlineChatWidget.scopedContextKeyService ?? this._contextKeyService; } + private _sessionCtor: CancelablePromise | undefined; + private _historyOffset: number = -1; + private _historyCandidate: string = ''; + private _historyUpdate: (prompt: string) => void; + + private _currentRequestId: string | undefined; + private _activeRequestCts?: CancellationTokenSource; + constructor( private readonly _instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, @ITerminalService private readonly _terminalService: ITerminalService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IChatService private readonly _chatService: IChatService, @IChatCodeBlockContextProviderService private readonly _chatCodeBlockContextProviderService: IChatCodeBlockContextProviderService, @IViewsService private readonly _viewsService: IViewsService, + @IStorageService private readonly _storageService: IStorageService, ) { super(); this._requestActiveContextKey = TerminalChatContextKeys.requestActive.bindTo(this._contextKeyService); - this._terminalAgentRegisteredContextKey = TerminalChatContextKeys.agentRegistered.bindTo(this._contextKeyService); this._responseContainsCodeBlockContextKey = TerminalChatContextKeys.responseContainsCodeBlock.bindTo(this._contextKeyService); this._responseContainsMulitpleCodeBlocksContextKey = TerminalChatContextKeys.responseContainsMultipleCodeBlocks.bindTo(this._contextKeyService); - this._responseSupportsIssueReportingContextKey = TerminalChatContextKeys.responseSupportsIssueReporting.bindTo(this._contextKeyService); - this._sessionResponseVoteContextKey = TerminalChatContextKeys.sessionResponseVote.bindTo(this._contextKeyService); - if (!this.initTerminalAgent()) { - this._register(this._chatAgentService.onDidChangeAgents(() => this.initTerminalAgent())); - } this._register(this._chatCodeBlockContextProviderService.registerProvider({ getCodeBlockContext: (editor) => { if (!editor || !this._chatWidget?.hasValue || !this.hasFocus()) { @@ -128,34 +126,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr } }, 'terminal')); - // TODO - // This is glue/debt that's needed while ChatModel isn't yet adopted. The chat model uses - // a default chat model (unless configured) and feedback is reported against that one. This - // code forwards the feedback to an actual registered provider - this._register(this._chatService.onDidPerformUserAction(e => { - // only forward feedback from the inline chat widget default model - if ( - this._chatWidget?.rawValue?.inlineChatWidget.usesDefaultChatModel - && e.sessionId === this._chatWidget?.rawValue?.inlineChatWidget.getChatModel().sessionId - ) { - if (e.action.kind === 'bug') { - this.acceptFeedback(undefined); - } else if (e.action.kind === 'vote') { - this.acceptFeedback(e.action.direction === ChatAgentVoteDirection.Up); - } + TerminalChatController._promptHistory = JSON.parse(this._storageService.get(TerminalChatController._storageKey, StorageScope.PROFILE, '[]')); + this._historyUpdate = (prompt: string) => { + const idx = TerminalChatController._promptHistory.indexOf(prompt); + if (idx >= 0) { + TerminalChatController._promptHistory.splice(idx, 1); } - })); - } - - private initTerminalAgent(): boolean { - const terminalAgent = this._chatAgentService.getAgentsByName(this._terminalAgentName)[0]; - if (terminalAgent) { - this._terminalAgentId = terminalAgent.id; - this._terminalAgentRegisteredContextKey.set(true); - return true; - } - - return false; + TerminalChatController._promptHistory.unshift(prompt); + this._historyOffset = -1; + this._historyCandidate = ''; + this._storageService.store(TerminalChatController._storageKey, JSON.stringify(TerminalChatController._promptHistory), StorageScope.PROFILE, StorageTarget.USER); + }; } xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { @@ -178,41 +159,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr }); } - acceptFeedback(helpful?: boolean): void { - const model = this._model.value; - if (!this._currentRequest || !model) { - return; - } - let action: ChatUserAction; - if (helpful === undefined) { - action = { kind: 'bug' }; - } else { - this._sessionResponseVoteContextKey.set(helpful ? 'up' : 'down'); - action = { kind: 'vote', direction: helpful ? ChatAgentVoteDirection.Up : ChatAgentVoteDirection.Down }; - } - // TODO:extract into helper method - for (const request of model.getRequests()) { - if (request.response?.response.value || request.response?.result) { - this._chatService.notifyUserAction({ - sessionId: request.session.sessionId, - requestId: request.id, - agentId: request.response?.agent?.id, - result: request.response?.result, - action - }); - } - } - this._chatWidget?.value.inlineChatWidget.updateStatus('Thank you for your feedback!', { resetAfter: 1250 }); - } + private async _createSession(): Promise { + this._sessionCtor = createCancelablePromise(async token => { + if (!this._model.value) { + this._model.value = this._chatService.startSession(ChatAgentLocation.Terminal, token); - cancel(): void { - if (this._currentRequest) { - this._model.value?.cancelRequest(this._currentRequest); - } - this._requestActiveContextKey.set(false); - this._chatWidget?.value.inlineChatWidget.updateProgress(false); - this._chatWidget?.value.inlineChatWidget.updateInfo(''); - this._chatWidget?.value.inlineChatWidget.updateToolbar(true); + if (!this._model.value) { + throw new Error('Failed to start chat session'); + } + } + }); + this._register(toDisposable(() => this._sessionCtor?.cancel())); } private _forcedPlaceholder: string | undefined = undefined; @@ -239,112 +196,65 @@ export class TerminalChatController extends Disposable implements ITerminalContr } clear(): void { - if (this._currentRequest) { - this._model.value?.cancelRequest(this._currentRequest); - } + this.cancel(); this._model.clear(); - this._chatWidget?.rawValue?.hide(); - this._chatWidget?.rawValue?.setValue(undefined); this._responseContainsCodeBlockContextKey.reset(); - this._sessionResponseVoteContextKey.reset(); this._requestActiveContextKey.reset(); + this._chatWidget?.value.hide(); + this._chatWidget?.value.setValue(undefined); } async acceptInput(): Promise { - if (!this._model.value) { - this._model.value = this._chatService.startSession(ChatAgentLocation.Terminal, CancellationToken.None); - if (!this._model.value) { - throw new Error('Could not start chat session'); - } - } - this._messages.fire(Message.ACCEPT_INPUT); - const model = this._model.value; - - this._lastInput = this._chatWidget?.value?.input(); - if (!this._lastInput) { + assertType(this._chatWidget); + assertType(this._model.value); + const lastInput = this._chatWidget.value.inlineChatWidget.value; + if (!lastInput) { return; } - - const responseCreated = new DeferredPromise(); - let responseCreatedComplete = false; - const completeResponseCreated = () => { - if (!responseCreatedComplete && this._currentRequest?.response) { - responseCreated.complete(this._currentRequest.response); - responseCreatedComplete = true; - } - }; - - const accessibilityRequestId = this._chatAccessibilityService.acceptRequest(); + const model = this._model.value; + this._chatWidget.value.inlineChatWidget.setChatModel(model); + this._historyUpdate(lastInput); + this._activeRequestCts?.cancel(); + this._activeRequestCts = new CancellationTokenSource(); + const store = new DisposableStore(); this._requestActiveContextKey.set(true); - const cancellationToken = new CancellationTokenSource().token; let responseContent = ''; - const progressCallback = (progress: IChatProgress) => { - if (cancellationToken.isCancellationRequested) { - return; - } - - if (progress.kind === 'markdownContent') { - responseContent += progress.content.value; - } - if (this._currentRequest) { - model.acceptResponseProgress(this._currentRequest, progress); - completeResponseCreated(); - } - }; - - await model.waitForInitialization(); - this._chatWidget?.value.addToHistory(this._lastInput); - const request: IParsedChatRequest = { - text: this._lastInput, - parts: [] - }; - const requestVarData: IChatRequestVariableData = { - variables: [] - }; - this._currentRequest = model.addRequest(request, requestVarData, 0); - completeResponseCreated(); - const requestProps: IChatAgentRequest = { - sessionId: model.sessionId, - requestId: this._currentRequest!.id, - agentId: this._terminalAgentId!, - message: this._lastInput, - variables: { variables: [] }, - location: ChatAgentLocation.Terminal - }; + const response = await this._chatWidget.value.inlineChatWidget.chatWidget.acceptInput(lastInput); + this._currentRequestId = response?.requestId; + const responsePromise = new DeferredPromise(); try { - const task = this._chatAgentService.invokeAgent(this._terminalAgentId!, requestProps, progressCallback, getHistoryEntriesFromModel(model, this._terminalAgentId!), cancellationToken); - this._chatWidget?.value.inlineChatWidget.updateChatMessage(undefined); - this._chatWidget?.value.inlineChatWidget.updateProgress(true); - this._chatWidget?.value.inlineChatWidget.updateInfo(GeneratingPhrase + '\u2026'); - await task; - } catch (e) { - - } finally { - this._requestActiveContextKey.set(false); - this._chatWidget?.value.inlineChatWidget.updateProgress(false); - this._chatWidget?.value.inlineChatWidget.updateInfo(''); - this._chatWidget?.value.inlineChatWidget.updateToolbar(true); - if (this._currentRequest) { - model.completeResponse(this._currentRequest); - completeResponseCreated(); - } - this._lastResponseContent = responseContent; - if (this._currentRequest) { - this._chatAccessibilityService.acceptResponse(responseContent, accessibilityRequestId); - const containsCode = responseContent.includes('```'); - this._chatWidget?.value.inlineChatWidget.updateChatMessage({ message: new MarkdownString(responseContent), requestId: this._currentRequest.id }, false, containsCode); - const firstCodeBlock = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(0); - const secondCodeBlock = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(1); - this._responseContainsCodeBlockContextKey.set(!!firstCodeBlock); - this._responseContainsMulitpleCodeBlocksContextKey.set(!!secondCodeBlock); - this._chatWidget?.value.inlineChatWidget.updateToolbar(true); - } - const supportIssueReporting = this._currentRequest?.response?.agent?.metadata?.supportIssueReporting; - if (supportIssueReporting !== undefined) { - this._responseSupportsIssueReportingContextKey.set(supportIssueReporting); + this._requestActiveContextKey.set(true); + if (response) { + store.add(response.onDidChange(async () => { + responseContent += response.response.value; + this._chatWidget?.value.inlineChatWidget.updateProgress(true); + if (response.isCanceled) { + this._requestActiveContextKey.set(false); + responsePromise.complete(undefined); + return; + } + if (response.isComplete) { + this._requestActiveContextKey.set(false); + this._requestActiveContextKey.set(false); + const containsCode = responseContent.includes('```'); + this._chatWidget!.value.inlineChatWidget.updateChatMessage({ message: new MarkdownString(responseContent), requestId: response!.requestId }, false, containsCode); + const firstCodeBlock = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(0); + const secondCodeBlock = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(1); + this._responseContainsCodeBlockContextKey.set(!!firstCodeBlock); + this._responseContainsMulitpleCodeBlocksContextKey.set(!!secondCodeBlock); + this._chatWidget?.value.inlineChatWidget.updateToolbar(true); + this._chatWidget?.value.inlineChatWidget.updateProgress(false); + responsePromise.complete(response); + } + })); } + await responsePromise.p; + return response; + } catch { + return; + } finally { + store.dispose(); } - return responseCreated.p; } updateInput(text: string, selectAll = true): void { @@ -369,6 +279,47 @@ export class TerminalChatController extends Disposable implements ITerminalContr return !!this._chatWidget?.rawValue?.hasFocus() ?? false; } + populateHistory(up: boolean) { + if (!this._chatWidget?.value) { + return; + } + + const len = TerminalChatController._promptHistory.length; + if (len === 0) { + return; + } + + if (this._historyOffset === -1) { + // remember the current value + this._historyCandidate = this._chatWidget.value.inlineChatWidget.value; + } + + const newIdx = this._historyOffset + (up ? 1 : -1); + if (newIdx >= len) { + // reached the end + return; + } + + let entry: string; + if (newIdx < 0) { + entry = this._historyCandidate; + this._historyOffset = -1; + } else { + entry = TerminalChatController._promptHistory[newIdx]; + this._historyOffset = newIdx; + } + + this._chatWidget.value.inlineChatWidget.value = entry; + this._chatWidget.value.inlineChatWidget.selectAll(); + } + + cancel(): void { + this._sessionCtor?.cancel(); + this._sessionCtor = undefined; + this._activeRequestCts?.cancel(); + this._requestActiveContextKey.set(false); + } + async acceptCommand(shouldExecute: boolean): Promise { const code = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(0); if (!code) { @@ -377,18 +328,22 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.value.acceptCommand(code.textEditorModel.getValue(), shouldExecute); } - reveal(): void { + async reveal(): Promise { + await this._createSession(); this._chatWidget?.value.reveal(); + this._chatWidget?.value.focus(); } async viewInChat(): Promise { + //TODO: is this necessary? better way? const widget = await showChatView(this._viewsService); - const request = this._currentRequest; - if (!widget || !request?.response) { + const currentRequest = this.chatWidget?.inlineChatWidget.chatWidget.viewModel?.model.getRequests().find(r => r.id === this._currentRequestId); + if (!widget || !currentRequest?.response) { return; } + const message: IChatProgress[] = []; - for (const item of request.response.response.value) { + for (const item of currentRequest.response.response.value) { if (item.kind === 'textEditGroup') { for (const group of item.edits) { message.push({ @@ -404,24 +359,15 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatService.addCompleteRequest(widget!.viewModel!.sessionId, // DEBT: Add hardcoded agent name until its removed - `@${this._terminalAgentName} ${request.message.text}`, - request.variableData, - request.attempt, + `@${this._terminalAgentName} ${currentRequest.message.text}`, + currentRequest.variableData, + currentRequest.attempt, { message, - result: request.response!.result, - followups: request.response!.followups + result: currentRequest.response!.result, + followups: currentRequest.response!.followups }); widget.focusLastMessage(); this._chatWidget?.rawValue?.hide(); } - - // TODO: Move to register calls, don't override - override dispose() { - if (this._currentRequest) { - this._model.value?.cancelRequest(this._currentRequest); - } - super.dispose(); - this.clear(); - } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts new file mode 100644 index 00000000000..fdcdf5c006f --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.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 { DisposableStore } from 'vs/base/common/lifecycle'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IChatAgentService, ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; + + +export class TerminalChatEnabler { + + static Id = 'terminalChat.enabler'; + + private readonly _ctxHasProvider: IContextKey; + + private readonly _store = new DisposableStore(); + + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @IChatAgentService chatAgentService: IChatAgentService + ) { + this._ctxHasProvider = TerminalChatContextKeys.hasChatAgent.bindTo(contextKeyService); + this._store.add(chatAgentService.onDidChangeAgents(() => { + const hasTerminalAgent = Boolean(chatAgentService.getDefaultAgent(ChatAgentLocation.Terminal)); + this._ctxHasProvider.set(hasTerminalAgent); + })); + } + + dispose() { + this._ctxHasProvider.reset(); + this._store.dispose(); + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 8b9aedb6bc9..33c01afbb63 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -16,11 +16,12 @@ import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { ITerminalInstance, type IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalStickyScrollContribution } from 'vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollContribution'; const enum Constants { - HorizontalMargin = 10 + HorizontalMargin = 10, + VerticalMargin = 30 } export class TerminalChatWidget extends Disposable { @@ -58,8 +59,6 @@ export class TerminalChatWidget extends Disposable { InlineChatWidget, ChatAgentLocation.Terminal, { - inputMenuId: MENU_TERMINAL_CHAT_INPUT, - widgetMenuId: MENU_TERMINAL_CHAT_WIDGET, statusMenuId: { menu: MENU_TERMINAL_CHAT_WIDGET_STATUS, options: { @@ -72,14 +71,20 @@ export class TerminalChatWidget extends Disposable { } } }, - feedbackMenuId: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, - telemetrySource: 'terminal-inline-chat', - rendererOptions: { editableCodeBlock: true } + chatWidgetViewOptions: { + rendererOptions: { editableCodeBlock: true }, + menus: { + telemetrySource: 'terminal-inline-chat', + executeToolbar: MENU_TERMINAL_CHAT_INPUT, + inputSideToolbar: MENU_TERMINAL_CHAT_WIDGET, + } + } } ); this._register(Event.any( this._inlineChatWidget.onDidChangeHeight, this._instance.onDimensionsChanged, + this._inlineChatWidget.chatWidget.onDidChangeContentHeight, Event.debounce(this._xterm.raw.onCursorMove, () => void 0, MicrotaskDelay), )(() => this._relayout())); @@ -91,11 +96,14 @@ export class TerminalChatWidget extends Disposable { this._container.appendChild(this._inlineChatWidget.domNode); this._focusTracker = this._register(trackFocus(this._container)); + this._register(this._focusTracker.onDidFocus(() => this._focusedContextKey.set(true))); this._register(this._focusTracker.onDidBlur(() => { + this._focusedContextKey.set(false); if (!this.inlineChatWidget.responseContent) { this.hide(); } })); + this.hide(); } @@ -115,15 +123,26 @@ export class TerminalChatWidget extends Disposable { const style = getActiveWindow().getComputedStyle(xtermElement); const xtermPadding = parseInt(style.paddingLeft) + parseInt(style.paddingRight); const width = Math.min(640, xtermElement.clientWidth - 12/* padding */ - 2/* border */ - Constants.HorizontalMargin - xtermPadding); - const height = Math.min(480, heightInPixel, this._getTerminalWrapperHeight() ?? Number.MAX_SAFE_INTEGER); + const terminalWrapperHeight = this._getTerminalWrapperHeight() ?? Number.MAX_SAFE_INTEGER; + let height = Math.min(480, heightInPixel, terminalWrapperHeight); + const top = this._getTop() ?? 0; if (width === 0 || height === 0) { return; } + + let adjustedHeight = undefined; + if (height < this._inlineChatWidget.contentHeight) { + if (height - top > 0) { + height = height - top - Constants.VerticalMargin; + } else { + height = height - Constants.VerticalMargin; + adjustedHeight = height; + } + } this._container.style.paddingLeft = style.paddingLeft; this._dimension = new Dimension(width, height); this._inlineChatWidget.layout(this._dimension); - - this._updateVerticalPosition(); + this._updateVerticalPosition(adjustedHeight); } private _reset() { @@ -134,13 +153,12 @@ export class TerminalChatWidget extends Disposable { reveal(): void { this._doLayout(this._inlineChatWidget.contentHeight); this._container.classList.remove('hide'); - this._focusedContextKey.set(true); this._visibleContextKey.set(true); this._inlineChatWidget.focus(); this._instance.scrollToBottom(); } - private _updateVerticalPosition(): void { + private _getTop(): number | undefined { const font = this._instance.xterm?.getFont(); if (!font?.charHeight) { return; @@ -149,14 +167,24 @@ export class TerminalChatWidget extends Disposable { const cellHeight = font.charHeight * font.lineHeight; const topPadding = terminalWrapperHeight - (this._instance.rows * cellHeight); const cursorY = (this._instance.xterm?.raw.buffer.active.cursorY ?? 0) + 1; - const top = topPadding + cursorY * cellHeight; + return topPadding + cursorY * cellHeight; + } + + private _updateVerticalPosition(adjustedHeight?: number): void { + const top = this._getTop(); + if (!top) { + return; + } this._container.style.top = `${top}px`; const widgetHeight = this._inlineChatWidget.contentHeight; + const terminalWrapperHeight = this._getTerminalWrapperHeight(); if (!terminalWrapperHeight) { return; } - if (top > terminalWrapperHeight - widgetHeight) { + if (top > terminalWrapperHeight - widgetHeight && terminalWrapperHeight - widgetHeight > 0) { this._setTerminalOffset(top - (terminalWrapperHeight - widgetHeight)); + } else if (adjustedHeight) { + this._setTerminalOffset(adjustedHeight); } else { this._setTerminalOffset(undefined); } @@ -168,12 +196,11 @@ export class TerminalChatWidget extends Disposable { hide(): void { this._container.classList.add('hide'); + this._inlineChatWidget.reset(); this._reset(); this._inlineChatWidget.updateChatMessage(undefined); this._inlineChatWidget.updateProgress(false); this._inlineChatWidget.updateToolbar(false); - this._inlineChatWidget.reset(); - this._focusedContextKey.set(false); this._visibleContextKey.set(false); this._inlineChatWidget.value = ''; this._instance.focus(); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts index 711ebbb18fe..41567ee5921 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts @@ -6,7 +6,6 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { localize } from 'vs/nls'; import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; -import product from 'vs/platform/product/common/product'; export const enum TerminalInitialHintSettingId { Enabled = 'terminal.integrated.initialHint' @@ -17,6 +16,6 @@ export const terminalInitialHintConfiguration: IStringDictionary parsedLink.suffix && link.text === parsedLink.path.text); + // Optimistically check that the link _starts with_ the parsed link text. If so, + // continue to use the parsed link + const matchingParsedLink = parsedLinks.find(parsedLink => parsedLink.suffix && link.text.startsWith(parsedLink.path.text)); if (matchingParsedLink) { if (matchingParsedLink.suffix?.row !== undefined) { + // Normalize the path based on the parsed link + text = matchingParsedLink.path.text; text += `:${matchingParsedLink.suffix.row}`; if (matchingParsedLink.suffix?.col !== undefined) { text += `:${matchingParsedLink.suffix.col}`; diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts index b419b7b94ca..80c28af6297 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import type { IBufferLine, IBufferCell } from '@xterm/xterm'; import { convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkHelpers'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/media/stickyScroll.css b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/media/stickyScroll.css index bc8e99fb00f..9f88f2757db 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/media/stickyScroll.css +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/media/stickyScroll.css @@ -11,6 +11,7 @@ z-index: 32; /* Must be higher than .xterm-viewport and decorations */ background: var(--vscode-terminalStickyScroll-background, var(--vscode-terminal-background, var(--vscode-panel-background))); box-shadow: var(--vscode-scrollbar-shadow) 0 3px 2px -2px; + border-bottom: 1px solid var(--vscode-terminalStickyScroll-border, transparent); } .part.sidebar .terminal-sticky-scroll, .part.auxiliarybar .terminal-sticky-scroll { diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollColorRegistry.ts b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollColorRegistry.ts index 5ab1af0d0eb..ed805826706 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollColorRegistry.ts +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollColorRegistry.ts @@ -3,20 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Color } from 'vs/base/common/color'; import { localize } from 'vs/nls'; import { registerColor } from 'vs/platform/theme/common/colorUtils'; -export const terminalStickyScrollBackground = registerColor('terminalStickyScroll.background', { - light: null, - dark: null, - hcDark: null, - hcLight: null -}, localize('terminalStickyScroll.background', 'The background color of the sticky scroll overlay in the terminal.')); +export const terminalStickyScrollBackground = registerColor('terminalStickyScroll.background', null, localize('terminalStickyScroll.background', 'The background color of the sticky scroll overlay in the terminal.')); export const terminalStickyScrollHoverBackground = registerColor('terminalStickyScrollHover.background', { dark: '#2A2D2E', light: '#F0F0F0', - hcDark: null, - hcLight: Color.fromHex('#0F4A85').transparent(0.1) + hcDark: '#E48B39', + hcLight: '#0f4a85' }, localize('terminalStickyScrollHover.background', 'The background color of the sticky scroll overlay in the terminal when hovered.')); + +registerColor('terminalStickyScroll.border', { + dark: null, + light: null, + hcDark: '#6fc3df', + hcLight: '#0f4a85' +}, localize('terminalStickyScroll.border', 'The border of the sticky scroll overlay in the terminal.')); diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index d46c7f3ae04..cd52628b6dc 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -16,7 +16,6 @@ import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { ITerminalConfigurationService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys'; import type { ITerminalAddon, Terminal } from '@xterm/xterm'; @@ -114,7 +113,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest private readonly _terminalSuggestWidgetVisibleContextKey: IContextKey, @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService ) { super(); @@ -159,7 +157,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest private _sync(promptInputState: IPromptInputModelState): void { const config = this._configurationService.getValue(terminalSuggestConfigSection); - const enabled = config.enabled || this._terminalConfigurationService.config.shellIntegration?.suggestEnabled; + const enabled = config.enabled; if (!enabled) { return; } @@ -222,11 +220,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } // TODO: What do frozen and auto do? const xtermBox = this._screen!.getBoundingClientRect(); - const panelBox = this._panel!.offsetParent!.getBoundingClientRect(); this._suggestWidget.showSuggestions(0, false, false, { - left: (xtermBox.left - panelBox.left) + this._terminal.buffer.active.cursorX * dimensions.width, - top: (xtermBox.top - panelBox.top) + this._terminal.buffer.active.cursorY * dimensions.height, + left: xtermBox.left + this._terminal.buffer.active.cursorX * dimensions.width, + top: xtermBox.top + this._terminal.buffer.active.cursorY * dimensions.height, height: dimensions.height }); } @@ -269,6 +266,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest if (!Array.isArray(completionList)) { completionList = [completionList]; } + const completions = completionList.map((e: any) => { return new SimpleCompletionItem({ label: e.ListItemText, @@ -285,7 +283,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest if (this._leadingLineContent.trim().includes(' ') || firstChar === '[') { replacementIndex = parseInt(args[0]); replacementLength = parseInt(args[1]); - this._leadingLineContent = completions[0]?.completion.label.slice(0, replacementLength) ?? ''; + const firstCompletion = completions[0]?.completion; + this._leadingLineContent = (firstCompletion?.completionText ?? firstCompletion?.label)?.slice(0, replacementLength) ?? ''; } else { completions.push(...this._cachedPwshCommands); } @@ -407,7 +406,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._leadingLineContent = completions[0].completion.label.slice(0, replacementLength); const model = new SimpleCompletionModel(completions, new LineContext(this._leadingLineContent, replacementIndex), replacementIndex, replacementLength); if (completions.length === 1) { - const insertText = completions[0].completion.label.substring(replacementLength); + const insertText = (completions[0].completion.completionText ?? completions[0].completion.label).substring(replacementLength); if (insertText.length === 0) { this._onBell.fire(); return; @@ -435,7 +434,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } // TODO: What do frozen and auto do? const xtermBox = this._screen!.getBoundingClientRect(); - const panelBox = this._panel!.offsetParent!.getBoundingClientRect(); this._initialPromptInputState = { value: this._promptInputModel.value, cursorIndex: this._promptInputModel.cursorIndex, @@ -443,8 +441,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest }; suggestWidget.setCompletionModel(model); suggestWidget.showSuggestions(0, false, false, { - left: (xtermBox.left - panelBox.left) + this._terminal.buffer.active.cursorX * dimensions.width, - top: (xtermBox.top - panelBox.top) + this._terminal.buffer.active.cursorY * dimensions.height, + left: xtermBox.left + this._terminal.buffer.active.cursorX * dimensions.width, + top: xtermBox.top + this._terminal.buffer.active.cursorY * dimensions.height, height: dimensions.height }); } @@ -505,6 +503,12 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest const completionText = completion.completionText ?? completion.label; const finalCompletionRightSide = completionText.substring((this._leadingLineContent?.length ?? 0) - (lastSpaceIndex === -1 ? 0 : lastSpaceIndex + 1)); + // Hide the widget if there is no change + if (finalCompletionRightSide === additionalInput) { + this.hideSuggestWidget(); + return; + } + // Get the final completion on the right side of the cursor if it differs from the initial // propmt input state let finalCompletionLeftSide = completionText.substring(0, (this._leadingLineContent?.length ?? 0) - (lastSpaceIndex === -1 ? 0 : lastSpaceIndex + 1)); diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index f465e37f076..357a6bbee60 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -10,7 +10,6 @@ import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; export const enum TerminalSuggestSettingId { Enabled = 'terminal.integrated.suggest.enabled', - EnabledLegacy = 'terminal.integrated.shellIntegration.suggestEnabled', QuickSuggestions = 'terminal.integrated.suggest.quickSuggestions', SuggestOnTriggerCharacters = 'terminal.integrated.suggest.suggestOnTriggerCharacters', } @@ -30,13 +29,6 @@ export const terminalSuggestConfiguration: IStringDictionary\",\"ResultType\":11,\"ToolTip\":\"System.Collections.ObjectModel.ReadOnlyCollection[T]\"},{\"CompletionText\":\"System.Collections.ReadOnlyCollectionBase\",\"ListItemText\":\"ReadOnlyCollectionBase\",\"ResultType\":11,\"ToolTip\":\"System.Collections.ReadOnlyCollectionBase\"},{\"CompletionText\":\"System.Runtime.CompilerServices.ReadOnlyCollectionBuilder\",\"ListItemText\":\"ReadOnlyCollectionBuilder<>\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.ReadOnlyCollectionBuilder[T]\"},{\"CompletionText\":\"System.Collections.ObjectModel.ReadOnlyDictionary\",\"ListItemText\":\"ReadOnlyDictionary<>\",\"ResultType\":11,\"ToolTip\":\"System.Collections.ObjectModel.ReadOnlyDictionary[T1, T2]\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReadOnlyDirectoryServerCollection\",\"ListItemText\":\"ReadOnlyDirectoryServerCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReadOnlyDirectoryServerCollection\"},{\"CompletionText\":\"System.Data.ReadOnlyException\",\"ListItemText\":\"ReadOnlyException\",\"ResultType\":11,\"ToolTip\":\"System.Data.ReadOnlyException\"},{\"CompletionText\":\"Json.Schema.ReadOnlyKeyword\",\"ListItemText\":\"ReadOnlyKeyword\",\"ResultType\":11,\"ToolTip\":\"Json.Schema.ReadOnlyKeyword\"},{\"CompletionText\":\"System.ReadOnlyMemory\",\"ListItemText\":\"ReadOnlyMemory<>\",\"ResultType\":11,\"ToolTip\":\"System.ReadOnlyMemory[T]\"},{\"CompletionText\":\"System.Net.Http.ReadOnlyMemoryContent\",\"ListItemText\":\"ReadOnlyMemoryContent\",\"ResultType\":11,\"ToolTip\":\"System.Net.Http.ReadOnlyMemoryContent\"},{\"CompletionText\":\"System.Collections.ObjectModel.ReadOnlyObservableCollection\",\"ListItemText\":\"ReadOnlyObservableCollection<>\",\"ResultType\":11,\"ToolTip\":\"System.Collections.ObjectModel.ReadOnlyObservableCollection[T]\"},{\"CompletionText\":\"System.Management.Automation.ReadOnlyPSMemberInfoCollection\",\"ListItemText\":\"ReadOnlyPSMemberInfoCollection<>\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.ReadOnlyPSMemberInfoCollection[T]\"},{\"CompletionText\":\"System.Buffers.ReadOnlySequence\",\"ListItemText\":\"ReadOnlySequence<>\",\"ResultType\":11,\"ToolTip\":\"System.Buffers.ReadOnlySequence[T]\"},{\"CompletionText\":\"System.Buffers.ReadOnlySequenceSegment\",\"ListItemText\":\"ReadOnlySequenceSegment<>\",\"ResultType\":11,\"ToolTip\":\"System.Buffers.ReadOnlySequenceSegment[T]\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReadOnlySiteCollection\",\"ListItemText\":\"ReadOnlySiteCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReadOnlySiteCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReadOnlySiteLinkBridgeCollection\",\"ListItemText\":\"ReadOnlySiteLinkBridgeCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReadOnlySiteLinkBridgeCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReadOnlySiteLinkCollection\",\"ListItemText\":\"ReadOnlySiteLinkCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReadOnlySiteLinkCollection\"},{\"CompletionText\":\"System.ReadOnlySpan\",\"ListItemText\":\"ReadOnlySpan<>\",\"ResultType\":11,\"ToolTip\":\"System.ReadOnlySpan[T]\"},{\"CompletionText\":\"System.Buffers.ReadOnlySpanAction\",\"ListItemText\":\"ReadOnlySpanAction<>\",\"ResultType\":11,\"ToolTip\":\"System.Buffers.ReadOnlySpanAction[T1, T2]\"},{\"CompletionText\":\"System.Runtime.InteropServices.Marshalling.ReadOnlySpanMarshaller\",\"ListItemText\":\"ReadOnlySpanMarshaller<>\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.InteropServices.Marshalling.ReadOnlySpanMarshaller[T1, T2]\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReadOnlyStringCollection\",\"ListItemText\":\"ReadOnlyStringCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReadOnlyStringCollection\"},{\"CompletionText\":\"System.Xml.ReadState\",\"ListItemText\":\"ReadState\",\"ResultType\":11,\"ToolTip\":\"System.Xml.ReadState\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.ReceiveJobCommand\",\"ListItemText\":\"ReceiveJobCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.ReceiveJobCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.ReceivePSSessionCommand\",\"ListItemText\":\"ReceivePSSessionCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.ReceivePSSessionCommand\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.RecipientInfo\",\"ListItemText\":\"RecipientInfo\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.RecipientInfo\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.RecipientInfoCollection\",\"ListItemText\":\"RecipientInfoCollection\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.RecipientInfoCollection\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.RecipientInfoEnumerator\",\"ListItemText\":\"RecipientInfoEnumerator\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.RecipientInfoEnumerator\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.RecipientInfoType\",\"ListItemText\":\"RecipientInfoType\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.RecipientInfoType\"},{\"CompletionText\":\"System.Speech.Recognition.RecognitionEventArgs\",\"ListItemText\":\"RecognitionEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognitionEventArgs\"},{\"CompletionText\":\"System.Speech.Recognition.RecognitionResult\",\"ListItemText\":\"RecognitionResult\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognitionResult\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizeCompletedEventArgs\",\"ListItemText\":\"RecognizeCompletedEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizeCompletedEventArgs\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizedAudio\",\"ListItemText\":\"RecognizedAudio\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizedAudio\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizedPhrase\",\"ListItemText\":\"RecognizedPhrase\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizedPhrase\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizedWordUnit\",\"ListItemText\":\"RecognizedWordUnit\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizedWordUnit\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizeMode\",\"ListItemText\":\"RecognizeMode\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizeMode\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizerInfo\",\"ListItemText\":\"RecognizerInfo\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizerInfo\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizerState\",\"ListItemText\":\"RecognizerState\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizerState\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizerUpdateReachedEventArgs\",\"ListItemText\":\"RecognizerUpdateReachedEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizerUpdateReachedEventArgs\"},{\"CompletionText\":\"System.ComponentModel.RecommendedAsConfigurableAttribute\",\"ListItemText\":\"RecommendedAsConfigurableAttribute\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RecommendedAsConfigurableAttribute\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RecordDeclarationSyntax\",\"ListItemText\":\"RecordDeclarationSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RecordDeclarationSyntax\"},{\"CompletionText\":\".Interop+Gdi32+RECT\",\"ListItemText\":\"RECT\",\"ResultType\":11,\"ToolTip\":\".Interop+Gdi32+RECT\"},{\"CompletionText\":\"System.Drawing.Rectangle\",\"ListItemText\":\"Rectangle\",\"ResultType\":11,\"ToolTip\":\"System.Drawing.Rectangle\"},{\"CompletionText\":\"System.Management.Automation.Host.Rectangle\",\"ListItemText\":\"Rectangle\",\"ResultType\":11,\"ToolTip\":\"Struct System.Management.Automation.Host.Rectangle\"},{\"CompletionText\":\"System.Drawing.RectangleConverter\",\"ListItemText\":\"RectangleConverter\",\"ResultType\":11,\"ToolTip\":\"System.Drawing.RectangleConverter\"},{\"CompletionText\":\"System.Drawing.RectangleF\",\"ListItemText\":\"RectangleF\",\"ResultType\":11,\"ToolTip\":\"System.Drawing.RectangleF\"},{\"CompletionText\":\"Json.Schema.RecursiveAnchorKeyword\",\"ListItemText\":\"RecursiveAnchorKeyword\",\"ResultType\":11,\"ToolTip\":\"Json.Schema.RecursiveAnchorKeyword\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RecursivePatternSyntax\",\"ListItemText\":\"RecursivePatternSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RecursivePatternSyntax\"},{\"CompletionText\":\"Json.Schema.RecursiveRefKeyword\",\"ListItemText\":\"RecursiveRefKeyword\",\"ResultType\":11,\"ToolTip\":\"Json.Schema.RecursiveRefKeyword\"},{\"CompletionText\":\"Microsoft.VisualBasic.FileIO.RecycleOption\",\"ListItemText\":\"RecycleOption\",\"ResultType\":11,\"ToolTip\":\"Microsoft.VisualBasic.FileIO.RecycleOption\"},{\"CompletionText\":\"System.Management.Automation.RedirectedException\",\"ListItemText\":\"RedirectedException\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RedirectedException\"},{\"CompletionText\":\"System.Management.Automation.Language.RedirectionAst\",\"ListItemText\":\"RedirectionAst\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Language.RedirectionAst\"},{\"CompletionText\":\"System.Management.Automation.Language.RedirectionStream\",\"ListItemText\":\"RedirectionStream\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Language.RedirectionStream\"},{\"CompletionText\":\"System.Management.Automation.Language.RedirectionToken\",\"ListItemText\":\"RedirectionToken\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Language.RedirectionToken\"},{\"CompletionText\":\"System.Security.Cryptography.Xml.Reference\",\"ListItemText\":\"Reference\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Xml.Reference\"},{\"CompletionText\":\"System.Runtime.CompilerServices.ReferenceAssemblyAttribute\",\"ListItemText\":\"ReferenceAssemblyAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.ReferenceAssemblyAttribute\"},{\"CompletionText\":\"System.ComponentModel.ReferenceConverter\",\"ListItemText\":\"ReferenceConverter\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.ReferenceConverter\"},{\"CompletionText\":\"System.ServiceModel.Syndication.ReferencedCategoriesDocument\",\"ListItemText\":\"ReferencedCategoriesDocument\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.Syndication.ReferencedCategoriesDocument\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.ReferenceDirectiveTriviaSyntax\",\"ListItemText\":\"ReferenceDirectiveTriviaSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.ReferenceDirectiveTriviaSyntax\"},{\"CompletionText\":\"System.Collections.Generic.ReferenceEqualityComparer\",\"ListItemText\":\"ReferenceEqualityComparer\",\"ResultType\":11,\"ToolTip\":\"System.Collections.Generic.ReferenceEqualityComparer\"},{\"CompletionText\":\"System.Text.Json.Serialization.ReferenceHandler\",\"ListItemText\":\"ReferenceHandler\",\"ResultType\":11,\"ToolTip\":\"System.Text.Json.Serialization.ReferenceHandler\"},{\"CompletionText\":\"System.Security.Cryptography.Xml.ReferenceList\",\"ListItemText\":\"ReferenceList\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Xml.ReferenceList\"},{\"CompletionText\":\"Newtonsoft.Json.ReferenceLoopHandling\",\"ListItemText\":\"ReferenceLoopHandling\",\"ResultType\":11,\"ToolTip\":\"Newtonsoft.Json.ReferenceLoopHandling\"},{\"CompletionText\":\"System.Text.Json.Serialization.ReferenceResolver\",\"ListItemText\":\"ReferenceResolver\",\"ResultType\":11,\"ToolTip\":\"System.Text.Json.Serialization.ReferenceResolver\"},{\"CompletionText\":\"System.DirectoryServices.Protocols.ReferralCallback\",\"ListItemText\":\"ReferralCallback\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.Protocols.ReferralCallback\"},{\"CompletionText\":\"System.DirectoryServices.ReferralChasingOption\",\"ListItemText\":\"ReferralChasingOption\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ReferralChasingOption\"},{\"CompletionText\":\"System.DirectoryServices.Protocols.ReferralChasingOptions\",\"ListItemText\":\"ReferralChasingOptions\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.Protocols.ReferralChasingOptions\"},{\"CompletionText\":\"Markdig.Extensions.ReferralLinks.ReferralLinksExtension\",\"ListItemText\":\"ReferralLinksExtension\",\"ResultType\":11,\"ToolTip\":\"Markdig.Extensions.ReferralLinks.ReferralLinksExtension\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefExpressionSyntax\",\"ListItemText\":\"RefExpressionSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefExpressionSyntax\"},{\"CompletionText\":\"Json.Schema.RefKeyword\",\"ListItemText\":\"RefKeyword\",\"ResultType\":11,\"ToolTip\":\"Json.Schema.RefKeyword\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.RefKind\",\"ListItemText\":\"RefKind\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.RefKind\"},{\"CompletionText\":\"Newtonsoft.Json.Serialization.ReflectionAttributeProvider\",\"ListItemText\":\"ReflectionAttributeProvider\",\"ResultType\":11,\"ToolTip\":\"Newtonsoft.Json.Serialization.ReflectionAttributeProvider\"},{\"CompletionText\":\"System.Reflection.ReflectionContext\",\"ListItemText\":\"ReflectionContext\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.ReflectionContext\"},{\"CompletionText\":\"System.ComponentModel.Composition.ReflectionModel.ReflectionModelServices\",\"ListItemText\":\"ReflectionModelServices\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.Composition.ReflectionModel.ReflectionModelServices\"},{\"CompletionText\":\"System.Security.Permissions.ReflectionPermission\",\"ListItemText\":\"ReflectionPermission\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.ReflectionPermission\"},{\"CompletionText\":\"System.Security.Permissions.ReflectionPermissionAttribute\",\"ListItemText\":\"ReflectionPermissionAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.ReflectionPermissionAttribute\"},{\"CompletionText\":\"System.Security.Permissions.ReflectionPermissionFlag\",\"ListItemText\":\"ReflectionPermissionFlag\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.ReflectionPermissionFlag\"},{\"CompletionText\":\"System.Reflection.ReflectionTypeLoadException\",\"ListItemText\":\"ReflectionTypeLoadException\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.ReflectionTypeLoadException\"},{\"CompletionText\":\"System.Management.Automation.Language.ReflectionTypeName\",\"ListItemText\":\"ReflectionTypeName\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Language.ReflectionTypeName\"},{\"CompletionText\":\"Newtonsoft.Json.Serialization.ReflectionValueProvider\",\"ListItemText\":\"ReflectionValueProvider\",\"ResultType\":11,\"ToolTip\":\"Newtonsoft.Json.Serialization.ReflectionValueProvider\"},{\"CompletionText\":\"System.ComponentModel.RefreshEventArgs\",\"ListItemText\":\"RefreshEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RefreshEventArgs\"},{\"CompletionText\":\"System.ComponentModel.RefreshEventHandler\",\"ListItemText\":\"RefreshEventHandler\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RefreshEventHandler\"},{\"CompletionText\":\"System.ComponentModel.RefreshProperties\",\"ListItemText\":\"RefreshProperties\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RefreshProperties\"},{\"CompletionText\":\"System.ComponentModel.RefreshPropertiesAttribute\",\"ListItemText\":\"RefreshPropertiesAttribute\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RefreshPropertiesAttribute\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RefSafetyRulesAttribute\",\"ListItemText\":\"RefSafetyRulesAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RefSafetyRulesAttribute\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefTypeExpressionSyntax\",\"ListItemText\":\"RefTypeExpressionSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefTypeExpressionSyntax\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefTypeSyntax\",\"ListItemText\":\"RefTypeSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefTypeSyntax\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefValueExpressionSyntax\",\"ListItemText\":\"RefValueExpressionSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefValueExpressionSyntax\"},{\"CompletionText\":\"regex\",\"ListItemText\":\"Regex\",\"ResultType\":11,\"ToolTip\":\"Class System.Text.RegularExpressions.Regex\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexCompilationInfo\",\"ListItemText\":\"RegexCompilationInfo\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexCompilationInfo\"},{\"CompletionText\":\"Newtonsoft.Json.Converters.RegexConverter\",\"ListItemText\":\"RegexConverter\",\"ResultType\":11,\"ToolTip\":\"Newtonsoft.Json.Converters.RegexConverter\"},{\"CompletionText\":\"Json.Schema.RegexFormat\",\"ListItemText\":\"RegexFormat\",\"ResultType\":11,\"ToolTip\":\"Json.Schema.RegexFormat\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexMatchTimeoutException\",\"ListItemText\":\"RegexMatchTimeoutException\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexMatchTimeoutException\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexOptions\",\"ListItemText\":\"RegexOptions\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexOptions\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexParseError\",\"ListItemText\":\"RegexParseError\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexParseError\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexParseException\",\"ListItemText\":\"RegexParseException\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexParseException\"},{\"CompletionText\":\"JetBrains.Annotations.RegexPatternAttribute\",\"ListItemText\":\"RegexPatternAttribute\",\"ResultType\":11,\"ToolTip\":\"JetBrains.Annotations.RegexPatternAttribute\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexRunner\",\"ListItemText\":\"RegexRunner\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexRunner\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexRunnerFactory\",\"ListItemText\":\"RegexRunnerFactory\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexRunnerFactory\"},{\"CompletionText\":\"System.Configuration.RegexStringValidator\",\"ListItemText\":\"RegexStringValidator\",\"ResultType\":11,\"ToolTip\":\"System.Configuration.RegexStringValidator\"},{\"CompletionText\":\"System.Configuration.RegexStringValidatorAttribute\",\"ListItemText\":\"RegexStringValidatorAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Configuration.RegexStringValidatorAttribute\"},{\"CompletionText\":\"System.Drawing.Region\",\"ListItemText\":\"Region\",\"ResultType\":11,\"ToolTip\":\"System.Drawing.Region\"},{\"CompletionText\":\"System.Drawing.Drawing2D.RegionData\",\"ListItemText\":\"RegionData\",\"ResultType\":11,\"ToolTip\":\"System.Drawing.Drawing2D.RegionData\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RegionDirectiveTriviaSyntax\",\"ListItemText\":\"RegionDirectiveTriviaSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RegionDirectiveTriviaSyntax\"},{\"CompletionText\":\"System.Globalization.RegionInfo\",\"ListItemText\":\"RegionInfo\",\"ResultType\":11,\"ToolTip\":\"System.Globalization.RegionInfo\"},{\"CompletionText\":\"System.Management.Automation.RegisterArgumentCompleterCommand\",\"ListItemText\":\"RegisterArgumentCompleterCommand\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RegisterArgumentCompleterCommand\"},{\"CompletionText\":\"System.Threading.RegisteredWaitHandle\",\"ListItemText\":\"RegisteredWaitHandle\",\"ResultType\":11,\"ToolTip\":\"System.Threading.RegisteredWaitHandle\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RegisterEngineEventCommand\",\"ListItemText\":\"RegisterEngineEventCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RegisterEngineEventCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RegisterObjectEventCommand\",\"ListItemText\":\"RegisterObjectEventCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RegisterObjectEventCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RegisterPSSessionConfigurationCommand\",\"ListItemText\":\"RegisterPSSessionConfigurationCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RegisterPSSessionConfigurationCommand\"},{\"CompletionText\":\"System.ComponentModel.Composition.Registration.RegistrationBuilder\",\"ListItemText\":\"RegistrationBuilder\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.Composition.Registration.RegistrationBuilder\"},{\"CompletionText\":\"Microsoft.Win32.Registry\",\"ListItemText\":\"Registry\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.Registry\"},{\"CompletionText\":\"System.Security.AccessControl.RegistryAccessRule\",\"ListItemText\":\"RegistryAccessRule\",\"ResultType\":11,\"ToolTip\":\"System.Security.AccessControl.RegistryAccessRule\"},{\"CompletionText\":\"Microsoft.Win32.RegistryAclExtensions\",\"ListItemText\":\"RegistryAclExtensions\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryAclExtensions\"},{\"CompletionText\":\"System.Security.AccessControl.RegistryAuditRule\",\"ListItemText\":\"RegistryAuditRule\",\"ResultType\":11,\"ToolTip\":\"System.Security.AccessControl.RegistryAuditRule\"},{\"CompletionText\":\"Microsoft.Win32.RegistryHive\",\"ListItemText\":\"RegistryHive\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryHive\"},{\"CompletionText\":\"Microsoft.Win32.RegistryKey\",\"ListItemText\":\"RegistryKey\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryKey\"},{\"CompletionText\":\"Microsoft.Win32.RegistryKeyPermissionCheck\",\"ListItemText\":\"RegistryKeyPermissionCheck\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryKeyPermissionCheck\"},{\"CompletionText\":\"Microsoft.Win32.RegistryOptions\",\"ListItemText\":\"RegistryOptions\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryOptions\"},{\"CompletionText\":\"System.Security.Permissions.RegistryPermission\",\"ListItemText\":\"RegistryPermission\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.RegistryPermission\"},{\"CompletionText\":\"System.Security.Permissions.RegistryPermissionAccess\",\"ListItemText\":\"RegistryPermissionAccess\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.RegistryPermissionAccess\"},{\"CompletionText\":\"System.Security.Permissions.RegistryPermissionAttribute\",\"ListItemText\":\"RegistryPermissionAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.RegistryPermissionAttribute\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RegistryProvider\",\"ListItemText\":\"RegistryProvider\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RegistryProvider\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RegistryProviderSetItemDynamicParameter\",\"ListItemText\":\"RegistryProviderSetItemDynamicParameter\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RegistryProviderSetItemDynamicParameter\"},{\"CompletionText\":\"System.Security.AccessControl.RegistryRights\",\"ListItemText\":\"RegistryRights\",\"ResultType\":11,\"ToolTip\":\"System.Security.AccessControl.RegistryRights\"},{\"CompletionText\":\"System.Security.AccessControl.RegistrySecurity\",\"ListItemText\":\"RegistrySecurity\",\"ResultType\":11,\"ToolTip\":\"System.Security.AccessControl.RegistrySecurity\"},{\"CompletionText\":\"Microsoft.Win32.RegistryValueKind\",\"ListItemText\":\"RegistryValueKind\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryValueKind\"},{\"CompletionText\":\"Microsoft.Win32.RegistryValueOptions\",\"ListItemText\":\"RegistryValueOptions\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryValueOptions\"},{\"CompletionText\":\"Microsoft.Win32.RegistryView\",\"ListItemText\":\"RegistryView\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryView\"},{\"CompletionText\":\"System.ComponentModel.DataAnnotations.RegularExpressionAttribute\",\"ListItemText\":\"RegularExpressionAttribute\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.DataAnnotations.RegularExpressionAttribute\"},{\"CompletionText\":\"System.Management.RelatedObjectQuery\",\"ListItemText\":\"RelatedObjectQuery\",\"ResultType\":11,\"ToolTip\":\"System.Management.RelatedObjectQuery\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RelationalPatternSyntax\",\"ListItemText\":\"RelationalPatternSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RelationalPatternSyntax\"},{\"CompletionText\":\"System.Management.RelationshipQuery\",\"ListItemText\":\"RelationshipQuery\",\"ResultType\":11,\"ToolTip\":\"System.Management.RelationshipQuery\"},{\"CompletionText\":\"Json.Pointer.RelativeJsonPointer\",\"ListItemText\":\"RelativeJsonPointer\",\"ResultType\":11,\"ToolTip\":\"Json.Pointer.RelativeJsonPointer\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.PooledObjects.PooledDelegates+Releaser\",\"ListItemText\":\"Releaser\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.PooledObjects.PooledDelegates+Releaser\"},{\"CompletionText\":\"System.Runtime.ConstrainedExecution.ReliabilityContractAttribute\",\"ListItemText\":\"ReliabilityContractAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.ConstrainedExecution.ReliabilityContractAttribute\"},{\"CompletionText\":\"System.ServiceModel.ReliableMessagingVersion\",\"ListItemText\":\"ReliableMessagingVersion\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.ReliableMessagingVersion\"},{\"CompletionText\":\"System.ServiceModel.ReliableSession\",\"ListItemText\":\"ReliableSession\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.ReliableSession\"},{\"CompletionText\":\"System.ServiceModel.Channels.ReliableSessionBindingElement\",\"ListItemText\":\"ReliableSessionBindingElement\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.Channels.ReliableSessionBindingElement\"},{\"CompletionText\":\"System.Net.Security.RemoteCertificateValidationCallback\",\"ListItemText\":\"RemoteCertificateValidationCallback\",\"ResultType\":11,\"ToolTip\":\"System.Net.Security.RemoteCertificateValidationCallback\"},{\"CompletionText\":\"System.Management.Automation.RemoteCommandInfo\",\"ListItemText\":\"RemoteCommandInfo\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RemoteCommandInfo\"},{\"CompletionText\":\"System.Management.Automation.RemoteException\",\"ListItemText\":\"RemoteException\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RemoteException\"},{\"CompletionText\":\"System.Management.Automation.Remoting.RemoteSessionNamedPipeServer\",\"ListItemText\":\"RemoteSessionNamedPipeServer\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Remoting.RemoteSessionNamedPipeServer\"},{\"CompletionText\":\"System.Management.Automation.RemoteStreamOptions\",\"ListItemText\":\"RemoteStreamOptions\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.RemoteStreamOptions\"},{\"CompletionText\":\"System.Management.Automation.RemotingBehavior\",\"ListItemText\":\"RemotingBehavior\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.RemotingBehavior\"},{\"CompletionText\":\"System.Management.Automation.RemotingCapability\",\"ListItemText\":\"RemotingCapability\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.RemotingCapability\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RemotingDebugRecord\",\"ListItemText\":\"RemotingDebugRecord\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RemotingDebugRecord\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RemotingErrorRecord\",\"ListItemText\":\"RemotingErrorRecord\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RemotingErrorRecord\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.Internal.RemotingErrorResources\",\"ListItemText\":\"RemotingErrorResources\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.Internal.RemotingErrorResources\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RemotingInformationRecord\",\"ListItemText\":\"RemotingInformationRecord\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RemotingInformationRecord\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RemotingProgressRecord\",\"ListItemText\":\"RemotingProgressRecord\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RemotingProgressRecord\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RemotingVerboseRecord\",\"ListItemText\":\"RemotingVerboseRecord\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RemotingVerboseRecord\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RemotingWarningRecord\",\"ListItemText\":\"RemotingWarningRecord\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RemotingWarningRecord\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveAliasCommand\",\"ListItemText\":\"RemoveAliasCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveAliasCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveEventCommand\",\"ListItemText\":\"RemoveEventCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveEventCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveItemCommand\",\"ListItemText\":\"RemoveItemCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveItemCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveItemPropertyCommand\",\"ListItemText\":\"RemoveItemPropertyCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveItemPropertyCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveJobCommand\",\"ListItemText\":\"RemoveJobCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveJobCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.RemoveKeyHandlerCommand\",\"ListItemText\":\"RemoveKeyHandlerCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.RemoveKeyHandlerCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveModuleCommand\",\"ListItemText\":\"RemoveModuleCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveModuleCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemovePSBreakpointCommand\",\"ListItemText\":\"RemovePSBreakpointCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemovePSBreakpointCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemovePSDriveCommand\",\"ListItemText\":\"RemovePSDriveCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemovePSDriveCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemovePSSessionCommand\",\"ListItemText\":\"RemovePSSessionCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemovePSSessionCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveServiceCommand\",\"ListItemText\":\"RemoveServiceCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveServiceCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveTypeDataCommand\",\"ListItemText\":\"RemoveTypeDataCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveTypeDataCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveVariableCommand\",\"ListItemText\":\"RemoveVariableCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveVariableCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RenameComputerChangeInfo\",\"ListItemText\":\"RenameComputerChangeInfo\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RenameComputerChangeInfo\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RenameComputerCommand\",\"ListItemText\":\"RenameComputerCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RenameComputerCommand\"},{\"CompletionText\":\"System.IO.RenamedEventArgs\",\"ListItemText\":\"RenamedEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.IO.RenamedEventArgs\"},{\"CompletionText\":\"System.IO.RenamedEventHandler\",\"ListItemText\":\"RenamedEventHandler\",\"ResultType\":11,\"ToolTip\":\"System.IO.RenamedEventHandler\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RenameItemCommand\",\"ListItemText\":\"RenameItemCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RenameItemCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RenameItemPropertyCommand\",\"ListItemText\":\"RenameItemPropertyCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RenameItemPropertyCommand\"},{\"CompletionText\":\"Markdig.Renderers.RendererBase\",\"ListItemText\":\"RendererBase\",\"ResultType\":11,\"ToolTip\":\"Markdig.Renderers.RendererBase\"},{\"CompletionText\":\"System.Speech.Recognition.ReplacementText\",\"ListItemText\":\"ReplacementText\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.ReplacementText\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationConnection\",\"ListItemText\":\"ReplicationConnection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationConnection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationConnectionCollection\",\"ListItemText\":\"ReplicationConnectionCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationConnectionCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationCursor\",\"ListItemText\":\"ReplicationCursor\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationCursor\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationCursorCollection\",\"ListItemText\":\"ReplicationCursorCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationCursorCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationFailure\",\"ListItemText\":\"ReplicationFailure\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationFailure\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationFailureCollection\",\"ListItemText\":\"ReplicationFailureCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationFailureCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationNeighbor\",\"ListItemText\":\"ReplicationNeighbor\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationNeighbor\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationNeighborCollection\",\"ListItemText\":\"ReplicationNeighborCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationNeighborCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationNeighbor+ReplicationNeighborOptions\",\"ListItemText\":\"ReplicationNeighborOptions\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationNeighbor+ReplicationNeighborOptions\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperation\",\"ListItemText\":\"ReplicationOperation\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperation\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperationCollection\",\"ListItemText\":\"ReplicationOperationCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperationCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperationInformation\",\"ListItemText\":\"ReplicationOperationInformation\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperationInformation\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperationType\",\"ListItemText\":\"ReplicationOperationType\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperationType\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationSecurityLevel\",\"ListItemText\":\"ReplicationSecurityLevel\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationSecurityLevel\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationSpan\",\"ListItemText\":\"ReplicationSpan\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationSpan\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.ReportDiagnostic\",\"ListItemText\":\"ReportDiagnostic\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.ReportDiagnostic\"},{\"CompletionText\":\"System.Management.Automation.Repository\",\"ListItemText\":\"Repository<>\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Repository[T]\"},{\"CompletionText\":\"Microsoft.ApplicationInsights.Extensibility.Implementation.Metrics.MetricTerms+Autocollection+Request\",\"ListItemText\":\"Request\",\"ResultType\":11,\"ToolTip\":\"Microsoft.ApplicationInsights.Extensibility.Implementation.Metrics.MetricTerms+Autocollection+Request\"},{\"CompletionText\":\"System.Net.Cache.RequestCacheLevel\",\"ListItemText\":\"RequestCacheLevel\",\"ResultType\":11,\"ToolTip\":\"System.Net.Cache.RequestCacheLevel\"},{\"CompletionText\":\"System.Net.Cache.RequestCachePolicy\",\"ListItemText\":\"RequestCachePolicy\",\"ResultType\":11,\"ToolTip\":\"System.Net.Cache.RequestCachePolicy\"},{\"CompletionText\":\"System.ServiceModel.Channels.RequestContext\",\"ListItemText\":\"RequestContext\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.Channels.RequestContext\"},{\"CompletionText\":\"Microsoft.ApplicationInsights.Extensibility.Implementation.Metrics.MetricTerms+Autocollection+Metric+RequestDuration\",\"ListItemText\":\"RequestDuration\",\"ResultType\":11,\"ToolTip\":\"Microsoft.ApplicationInsights.Extensibility.Implementation.Metrics.MetricTerms+Autocollection+Metric+RequestDuration\"},{\"CompletionText\":\"Microsoft.ApplicationInsights.DataContracts.RequestTelemetry\",\"ListItemText\":\"RequestTelemetry\",\"ResultType\":11,\"ToolTip\":\"Microsoft.ApplicationInsights.DataContracts.RequestTelemetry\"},{\"CompletionText\":\"Newtonsoft.Json.Required\",\"ListItemText\":\"Required\",\"ResultType\":11,\"ToolTip\":\"Newtonsoft.Json.Required\"},{\"CompletionText\":\"System.ComponentModel.DataAnnotations.RequiredAttribute\",\"ListItemText\":\"RequiredAttribute\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.DataAnnotations.RequiredAttribute\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RequiredAttributeAttribute\",\"ListItemText\":\"RequiredAttributeAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RequiredAttributeAttribute\"},{\"CompletionText\":\"Json.Schema.RequiredKeyword\",\"ListItemText\":\"RequiredKeyword\",\"ResultType\":11,\"ToolTip\":\"Json.Schema.RequiredKeyword\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RequiredMemberAttribute\",\"ListItemText\":\"RequiredMemberAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RequiredMemberAttribute\"},{\"CompletionText\":\"System.Diagnostics.CodeAnalysis.RequiresAssemblyFilesAttribute\",\"ListItemText\":\"RequiresAssemblyFilesAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Diagnostics.CodeAnalysis.RequiresAssemblyFilesAttribute\"},{\"CompletionText\":\"System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute\",\"ListItemText\":\"RequiresDynamicCodeAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RequiresLocationAttribute\",\"ListItemText\":\"RequiresLocationAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RequiresLocationAttribute\"},{\"CompletionText\":\"System.Runtime.Versioning.RequiresPreviewFeaturesAttribute\",\"ListItemText\":\"RequiresPreviewFeaturesAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.Versioning.RequiresPreviewFeaturesAttribute\"},{\"CompletionText\":\"JetBrains.Annotations.RequireStaticDelegateAttribute\",\"ListItemText\":\"RequireStaticDelegateAttribute\",\"ResultType\":11,\"ToolTip\":\"JetBrains.Annotations.RequireStaticDelegateAttribute\"},{\"CompletionText\":\"System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute\",\"ListItemText\":\"RequiresUnreferencedCodeAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute\"},{\"CompletionText\":\"System.Reflection.Metadata.ReservedBlob\",\"ListItemText\":\"ReservedBlob<>\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.Metadata.ReservedBlob[T]\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.ResetCapability\",\"ListItemText\":\"ResetCapability\",\"ResultType\":11,\"ToolTip\":\"Enum Microsoft.PowerShell.Commands.ResetCapability\"},{\"CompletionText\":\"System.Management.Automation.ResolutionPurpose\",\"ListItemText\":\"ResolutionPurpose\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.ResolutionPurpose\"},{\"CompletionText\":\"System.ResolveEventArgs\",\"ListItemText\":\"ResolveEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.ResolveEventArgs\"},{\"CompletionText\":\"System.ResolveEventHandler\",\"ListItemText\":\"ResolveEventHandler\",\"ResultType\":11,\"ToolTip\":\"System.ResolveEventHandler\"},{\"CompletionText\":\"System.ComponentModel.Design.Serialization.ResolveNameEventArgs\",\"ListItemText\":\"ResolveNameEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.Design.Serialization.ResolveNameEventArgs\"},{\"CompletionText\":\"System.ComponentModel.Design.Serialization.ResolveNameEventHandler\",\"ListItemText\":\"ResolveNameEventHandler\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.Design.Serialization.ResolveNameEventHandler\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.ResolvePathCommand\",\"ListItemText\":\"ResolvePathCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.ResolvePathCommand\"},{\"CompletionText\":\"System.Reflection.ResourceAttributes\",\"ListItemText\":\"ResourceAttributes\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.ResourceAttributes\"},{\"CompletionText\":\"System.ServiceModel.Syndication.ResourceCollectionInfo\",\"ListItemText\":\"ResourceCollectionInfo\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.Syndication.ResourceCollectionInfo\"},{\"CompletionText\":\"System.Runtime.Versioning.ResourceConsumptionAttribute\",\"ListItemText\":\"ResourceConsumptionAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.Versioning.ResourceConsumptionAttribute\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.ResourceDescription\",\"ListItemText\":\"ResourceDescription\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.ResourceDescription\"},{\"CompletionText\":\"System.Runtime.Versioning.ResourceExposureAttribute\",\"ListItemText\":\"ResourceExposureAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.Versioning.ResourceExposureAttribute\"},{\"CompletionText\":\"System.Reflection.ResourceLocation\",\"ListItemText\":\"ResourceLocation\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.ResourceLocation\"},{\"CompletionText\":\"System.Resources.ResourceManager\",\"ListItemText\":\"ResourceManager\",\"ResultType\":11,\"ToolTip\":\"System.Resources.ResourceManager\"},{\"CompletionText\":\"System.Security.Permissions.ResourcePermissionBase\",\"ListItemText\":\"ResourcePermissionBase\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.ResourcePermissionBase\"},{\"CompletionText\":\"System.Security.Permissions.ResourcePermissionBaseEntry\",\"ListItemText\":\"ResourcePermissionBaseEntry\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.ResourcePermissionBaseEntry\"},{\"CompletionText\":\"System.Resources.ResourceReader\",\"ListItemText\":\"ResourceReader\",\"ResultType\":11,\"ToolTip\":\"System.Resources.ResourceReader\"},{\"CompletionText\":\"System.Runtime.Versioning.ResourceScope\",\"ListItemText\":\"ResourceScope\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.Versioning.ResourceScope\"},{\"CompletionText\":\"System.Reflection.PortableExecutable.ResourceSectionBuilder\",\"ListItemText\":\"ResourceSectionBuilder\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.PortableExecutable.ResourceSectionBuilder\"},{\"CompletionText\":\"System.Resources.ResourceSet\",\"ListItemText\":\"ResourceSet\",\"ResultType\":11,\"ToolTip\":\"System.Resources.ResourceSet\"},{\"CompletionText\":\"System.Security.AccessControl.ResourceType\",\"ListItemText\":\"ResourceType\",\"ResultType\":11,\"ToolTip\":\"System.Security.AccessControl.ResourceType\"},{\"CompletionText\":\"System.Resources.ResourceWriter\",\"ListItemText\":\"ResourceWriter\",\"ResultType\":11,\"ToolTip\":\"System.Resources.ResourceWriter\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RestartComputerCommand\",\"ListItemText\":\"RestartComputerCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RestartComputerCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RestartComputerTimeoutException\",\"ListItemText\":\"RestartComputerTimeoutException\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RestartComputerTimeoutException\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RestartServiceCommand\",\"ListItemText\":\"RestartServiceCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RestartServiceCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.InvokeRestMethodCommand+RestReturnType\",\"ListItemText\":\"RestReturnType\",\"ResultType\":11,\"ToolTip\":\"Enum Microsoft.PowerShell.Commands.InvokeRestMethodCommand+RestReturnType\"},{\"CompletionText\":\"System.DirectoryServices.Protocols.ResultCode\",\"ListItemText\":\"ResultCode\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.Protocols.ResultCode\"},{\"CompletionText\":\"System.DirectoryServices.ResultPropertyCollection\",\"ListItemText\":\"ResultPropertyCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ResultPropertyCollection\"},{\"CompletionText\":\"System.DirectoryServices.ResultPropertyValueCollection\",\"ListItemText\":\"ResultPropertyValueCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ResultPropertyValueCollection\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.ResumeServiceCommand\",\"ListItemText\":\"ResumeServiceCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.ResumeServiceCommand\"},{\"CompletionText\":\"System.Data.Odbc.ODBC32+RETCODE\",\"ListItemText\":\"RETCODE\",\"ResultType\":11,\"ToolTip\":\"System.Data.Odbc.ODBC32+RETCODE\"},{\"CompletionText\":\"System.Net.Http.Headers.RetryConditionHeaderValue\",\"ListItemText\":\"RetryConditionHeaderValue\",\"ResultType\":11,\"ToolTip\":\"System.Net.Http.Headers.RetryConditionHeaderValue\"},{\"CompletionText\":\"System.Management.Automation.ReturnContainers\",\"ListItemText\":\"ReturnContainers\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.ReturnContainers\"},{\"CompletionText\":\"System.Management.Automation.Language.ReturnStatementAst\",\"ListItemText\":\"ReturnStatementAst\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Language.ReturnStatementAst\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.ReturnStatementSyntax\",\"ListItemText\":\"ReturnStatementSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.ReturnStatementSyntax\"},{\"CompletionText\":\"System.Reflection.Metadata.Ecma335.ReturnTypeEncoder\",\"ListItemText\":\"ReturnTypeEncoder\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.Metadata.Ecma335.ReturnTypeEncoder\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.IOperation+OperationList+Reversed\",\"ListItemText\":\"Reversed\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.IOperation+OperationList+Reversed\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.ChildSyntaxList+Reversed\",\"ListItemText\":\"Reversed\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.ChildSyntaxList+Reversed\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.SyntaxTriviaList+Reversed\",\"ListItemText\":\"Reversed\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.SyntaxTriviaList+Reversed\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.SyntaxTokenList+Reversed\",\"ListItemText\":\"Reversed\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.SyntaxTokenList+Reversed\"},{\"CompletionText\":\"System.Security.Cryptography.Rfc2898DeriveBytes\",\"ListItemText\":\"Rfc2898DeriveBytes\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Rfc2898DeriveBytes\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.Rfc3161TimestampRequest\",\"ListItemText\":\"Rfc3161TimestampRequest\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.Rfc3161TimestampRequest\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.Rfc3161TimestampToken\",\"ListItemText\":\"Rfc3161TimestampToken\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.Rfc3161TimestampToken\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.Rfc3161TimestampTokenInfo\",\"ListItemText\":\"Rfc3161TimestampTokenInfo\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.Rfc3161TimestampTokenInfo\"},{\"CompletionText\":\"System.Security.Cryptography.Rijndael\",\"ListItemText\":\"Rijndael\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Rijndael\"},{\"CompletionText\":\"System.Security.Cryptography.RijndaelManaged\",\"ListItemText\":\"RijndaelManaged\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RijndaelManaged\"},{\"CompletionText\":\"System.Security.Cryptography.RNGCryptoServiceProvider\",\"ListItemText\":\"RNGCryptoServiceProvider\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RNGCryptoServiceProvider\"},{\"CompletionText\":\"System.Management.Automation.RollbackSeverity\",\"ListItemText\":\"RollbackSeverity\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.RollbackSeverity\"},{\"CompletionText\":\"System.ComponentModel.Design.Serialization.RootDesignerSerializerAttribute\",\"ListItemText\":\"RootDesignerSerializerAttribute\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.Design.Serialization.RootDesignerSerializerAttribute\"},{\"CompletionText\":\"Roslyn\",\"ListItemText\":\"Roslyn\",\"ResultType\":10,\"ToolTip\":\"Namespace Roslyn\"},{\"CompletionText\":\"System.Drawing.RotateFlipType\",\"ListItemText\":\"RotateFlipType\",\"ResultType\":11,\"ToolTip\":\"System.Drawing.RotateFlipType\"},{\"CompletionText\":\"Markdig.Renderers.Roundtrip.Inlines.RoundtripHtmlEntityInlineRenderer\",\"ListItemText\":\"RoundtripHtmlEntityInlineRenderer\",\"ResultType\":11,\"ToolTip\":\"Markdig.Renderers.Roundtrip.Inlines.RoundtripHtmlEntityInlineRenderer\"},{\"CompletionText\":\"Markdig.Renderers.Roundtrip.Inlines.RoundtripHtmlInlineRenderer\",\"ListItemText\":\"RoundtripHtmlInlineRenderer\",\"ResultType\":11,\"ToolTip\":\"Markdig.Renderers.Roundtrip.Inlines.RoundtripHtmlInlineRenderer\"},{\"CompletionText\":\"Markdig.Renderers.Roundtrip.RoundtripObjectRenderer\",\"ListItemText\":\"RoundtripObjectRenderer<>\",\"ResultType\":11,\"ToolTip\":\"Markdig.Renderers.Roundtrip.RoundtripObjectRenderer[T]\"},{\"CompletionText\":\"Markdig.Renderers.Roundtrip.RoundtripRenderer\",\"ListItemText\":\"RoundtripRenderer\",\"ResultType\":11,\"ToolTip\":\"Markdig.Renderers.Roundtrip.RoundtripRenderer\"},{\"CompletionText\":\"JetBrains.Annotations.RouteParameterConstraintAttribute\",\"ListItemText\":\"RouteParameterConstraintAttribute\",\"ResultType\":11,\"ToolTip\":\"JetBrains.Annotations.RouteParameterConstraintAttribute\"},{\"CompletionText\":\"JetBrains.Annotations.RouteTemplateAttribute\",\"ListItemText\":\"RouteTemplateAttribute\",\"ResultType\":11,\"ToolTip\":\"JetBrains.Annotations.RouteTemplateAttribute\"},{\"CompletionText\":\"System.Data.RowNotInTableException\",\"ListItemText\":\"RowNotInTableException\",\"ResultType\":11,\"ToolTip\":\"System.Data.RowNotInTableException\"},{\"CompletionText\":\"System.Data.Common.RowUpdatedEventArgs\",\"ListItemText\":\"RowUpdatedEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.Data.Common.RowUpdatedEventArgs\"},{\"CompletionText\":\"System.Data.Common.RowUpdatingEventArgs\",\"ListItemText\":\"RowUpdatingEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.Data.Common.RowUpdatingEventArgs\"},{\"CompletionText\":\"System.Security.Cryptography.RSA\",\"ListItemText\":\"RSA\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSA\"},{\"CompletionText\":\"System.Security.Cryptography.X509Certificates.RSACertificateExtensions\",\"ListItemText\":\"RSACertificateExtensions\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.X509Certificates.RSACertificateExtensions\"},{\"CompletionText\":\"System.Security.Cryptography.RSACng\",\"ListItemText\":\"RSACng\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSACng\"},{\"CompletionText\":\"System.Security.Cryptography.RSACryptoServiceProvider\",\"ListItemText\":\"RSACryptoServiceProvider\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSACryptoServiceProvider\"},{\"CompletionText\":\"System.Security.Cryptography.RSAEncryptionPadding\",\"ListItemText\":\"RSAEncryptionPadding\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAEncryptionPadding\"},{\"CompletionText\":\"System.Security.Cryptography.RSAEncryptionPaddingMode\",\"ListItemText\":\"RSAEncryptionPaddingMode\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAEncryptionPaddingMode\"},{\"CompletionText\":\"System.Security.Cryptography.Xml.RSAKeyValue\",\"ListItemText\":\"RSAKeyValue\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Xml.RSAKeyValue\"},{\"CompletionText\":\"System.Security.Cryptography.RSAOAEPKeyExchangeDeformatter\",\"ListItemText\":\"RSAOAEPKeyExchangeDeformatter\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAOAEPKeyExchangeDeformatter\"},{\"CompletionText\":\"System.Security.Cryptography.RSAOAEPKeyExchangeFormatter\",\"ListItemText\":\"RSAOAEPKeyExchangeFormatter\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAOAEPKeyExchangeFormatter\"},{\"CompletionText\":\"System.Security.Cryptography.RSAOpenSsl\",\"ListItemText\":\"RSAOpenSsl\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAOpenSsl\"},{\"CompletionText\":\"System.Security.Cryptography.RSAParameters\",\"ListItemText\":\"RSAParameters\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAParameters\"},{\"CompletionText\":\"System.Security.Cryptography.RSAPKCS1KeyExchangeDeformatter\",\"ListItemText\":\"RSAPKCS1KeyExchangeDeformatter\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAPKCS1KeyExchangeDeformatter\"},{\"CompletionText\":\"System.Security.Cryptography.RSAPKCS1KeyExchangeFormatter\",\"ListItemText\":\"RSAPKCS1KeyExchangeFormatter\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAPKCS1KeyExchangeFormatter\"},{\"CompletionText\":\"System.Security.Cryptography.RSAPKCS1SignatureDeformatter\",\"ListItemText\":\"RSAPKCS1SignatureDeformatter\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAPKCS1SignatureDeformatter\"},{\"CompletionText\":\"System.Security.Cryptography.RSAPKCS1SignatureFormatter\",\"ListItemText\":\"RSAPKCS1SignatureFormatter\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAPKCS1SignatureFormatter\"},{\"CompletionText\":\"System.Configuration.RsaProtectedConfigurationProvider\",\"ListItemText\":\"RsaProtectedConfigurationProvider\",\"ResultType\":11,\"ToolTip\":\"System.Configuration.RsaProtectedConfigurationProvider\"},{\"CompletionText\":\"System.Security.Cryptography.RSASignaturePadding\",\"ListItemText\":\"RSASignaturePadding\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSASignaturePadding\"},{\"CompletionText\":\"System.Security.Cryptography.RSASignaturePaddingMode\",\"ListItemText\":\"RSASignaturePaddingMode\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSASignaturePaddingMode\"},{\"CompletionText\":\"System.ServiceModel.Syndication.Rss20FeedFormatter\",\"ListItemText\":\"Rss20FeedFormatter\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.Syndication.Rss20FeedFormatter\"},{\"CompletionText\":\"System.ServiceModel.Syndication.Rss20ItemFormatter\",\"ListItemText\":\"Rss20ItemFormatter\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.Syndication.Rss20ItemFormatter\"},{\"CompletionText\":\"System.Data.Rule\",\"ListItemText\":\"Rule\",\"ResultType\":11,\"ToolTip\":\"System.Data.Rule\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RuleCache\",\"ListItemText\":\"RuleCache<>\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RuleCache[T]\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.RuleSet\",\"ListItemText\":\"RuleSet\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.RuleSet\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.RuleSetInclude\",\"ListItemText\":\"RuleSetInclude\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.RuleSetInclude\"},{\"CompletionText\":\"System.Text.Rune\",\"ListItemText\":\"Rune\",\"ResultType\":11,\"ToolTip\":\"System.Text.Rune\"},{\"CompletionText\":\"System.ComponentModel.RunInstallerAttribute\",\"ListItemText\":\"RunInstallerAttribute\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RunInstallerAttribute\"},{\"CompletionText\":\"runspace\",\"ListItemText\":\"Runspace\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.Runspace\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceAttribute\",\"ListItemText\":\"RunspaceAttribute\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceAttribute\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceAvailability\",\"ListItemText\":\"RunspaceAvailability\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Runspaces.RunspaceAvailability\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceAvailabilityEventArgs\",\"ListItemText\":\"RunspaceAvailabilityEventArgs\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceAvailabilityEventArgs\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceCapability\",\"ListItemText\":\"RunspaceCapability\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Runspaces.RunspaceCapability\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceConnectionInfo\",\"ListItemText\":\"RunspaceConnectionInfo\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceConnectionInfo\"},{\"CompletionText\":\"runspacefactory\",\"ListItemText\":\"RunspaceFactory\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceFactory\"},{\"CompletionText\":\"System.Management.Automation.RunspaceMode\",\"ListItemText\":\"RunspaceMode\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.RunspaceMode\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceOpenModuleLoadException\",\"ListItemText\":\"RunspaceOpenModuleLoadException\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceOpenModuleLoadException\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspacePool\",\"ListItemText\":\"RunspacePool\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspacePool\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspacePoolAvailability\",\"ListItemText\":\"RunspacePoolAvailability\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Runspaces.RunspacePoolAvailability\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspacePoolCapability\",\"ListItemText\":\"RunspacePoolCapability\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Runspaces.RunspacePoolCapability\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspacePoolState\",\"ListItemText\":\"RunspacePoolState\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Runspaces.RunspacePoolState\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspacePoolStateChangedEventArgs\",\"ListItemText\":\"RunspacePoolStateChangedEventArgs\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspacePoolStateChangedEventArgs\"},{\"CompletionText\":\"System.Management.Automation.RunspacePoolStateInfo\",\"ListItemText\":\"RunspacePoolStateInfo\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RunspacePoolStateInfo\"},{\"CompletionText\":\"System.Management.Automation.RunspaceRepository\",\"ListItemText\":\"RunspaceRepository\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RunspaceRepository\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceState\",\"ListItemText\":\"RunspaceState\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Runspaces.RunspaceState\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceStateEventArgs\",\"ListItemText\":\"RunspaceStateEventArgs\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceStateEventArgs\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceStateInfo\",\"ListItemText\":\"RunspaceStateInfo\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceStateInfo\"},{\"CompletionText\":\"System.RuntimeArgumentHandle\",\"ListItemText\":\"RuntimeArgumentHandle\",\"ResultType\":11,\"ToolTip\":\"System.RuntimeArgumentHandle\"},{\"CompletionText\":\"Microsoft.CSharp.RuntimeBinder.RuntimeBinderException\",\"ListItemText\":\"RuntimeBinderException\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CSharp.RuntimeBinder.RuntimeBinderException\"},{\"CompletionText\":\"Microsoft.CSharp.RuntimeBinder.RuntimeBinderInternalCompilerException\",\"ListItemText\":\"RuntimeBinderInternalCompilerException\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CSharp.RuntimeBinder.RuntimeBinderInternalCompilerException\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.RuntimeCapability\",\"ListItemText\":\"RuntimeCapability\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.RuntimeCapability\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RuntimeCompatibilityAttribute\",\"ListItemText\":\"RuntimeCompatibilityAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RuntimeCompatibilityAttribute\"},{\"CompletionText\":\"System.Management.Automation.RuntimeDefinedParameter\",\"ListItemText\":\"RuntimeDefinedParameter\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RuntimeDefinedParameter\"},{\"CompletionText\":\"System.Management.Automation.RuntimeDefinedParameterDictionary\",\"ListItemText\":\"RuntimeDefinedParameterDictionary\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RuntimeDefinedParameterDictionary\"},{\"CompletionText\":\"System.Runtime.InteropServices.RuntimeEnvironment\",\"ListItemText\":\"RuntimeEnvironment\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.InteropServices.RuntimeEnvironment\"},{\"CompletionText\":\"System.Management.Automation.RuntimeException\",\"ListItemText\":\"RuntimeException\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RuntimeException\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RuntimeFeature\",\"ListItemText\":\"RuntimeFeature\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RuntimeFeature\"},{\"CompletionText\":\"System.RuntimeFieldHandle\",\"ListItemText\":\"RuntimeFieldHandle\",\"ResultType\":11,\"ToolTip\":\"System.RuntimeFieldHandle\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RuntimeHelpers\",\"ListItemText\":\"RuntimeHelpers\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RuntimeHelpers\"},{\"CompletionText\":\"System.Runtime.InteropServices.RuntimeInformation\",\"ListItemText\":\"RuntimeInformation\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.InteropServices.RuntimeInformation\"},{\"CompletionText\":\"System.RuntimeMethodHandle\",\"ListItemText\":\"RuntimeMethodHandle\",\"ResultType\":11,\"ToolTip\":\"System.RuntimeMethodHandle\"},{\"CompletionText\":\"System.Reflection.RuntimeReflectionExtensions\",\"ListItemText\":\"RuntimeReflectionExtensions\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.RuntimeReflectionExtensions\"},{\"CompletionText\":\"System.RuntimeTypeHandle\",\"ListItemText\":\"RuntimeTypeHandle\",\"ResultType\":11,\"ToolTip\":\"System.RuntimeTypeHandle\"},{\"CompletionText\":\"System.Linq.Expressions.RuntimeVariablesExpression\",\"ListItemText\":\"RuntimeVariablesExpression\",\"ResultType\":11,\"ToolTip\":\"System.Linq.Expressions.RuntimeVariablesExpression\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RuntimeWrappedException\",\"ListItemText\":\"RuntimeWrappedException\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RuntimeWrappedException\"},{\"CompletionText\":\"System.ComponentModel.RunWorkerCompletedEventArgs\",\"ListItemText\":\"RunWorkerCompletedEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RunWorkerCompletedEventArgs\"},{\"CompletionText\":\"System.ComponentModel.RunWorkerCompletedEventHandler\",\"ListItemText\":\"RunWorkerCompletedEventHandler\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RunWorkerCompletedEventHandler\"}]\u0007" }, + { + "type": "input", + "data": "r" + }, { "type": "input", "data": "e" }, + { + "type": "input", + "data": "q" + }, { "type": "output", - "data": "\u001b[?25l\u001b[3;3H[re\u001b[?25h" + "data": "\u001b[?25l\u001b[3;3H[req\u001b[?25h" }, { "type": "promptInputChange", - "data": "[re|" + "data": "[req|" }, { "type": "command", @@ -99,14 +107,14 @@ export const events = [ }, { "type": "sendText", - "data": "System.Xml.Linq.ReaderOptions" + "data": "Json.Schema.RequiredKeyword" }, { "type": "output", - "data": "\u001b[?25l\u001b[3;3H[System.Xml.Linq.ReaderOptions\u001b[?25h" + "data": "\u001b[?25l\u001b[3;3H[Json.Schema.RequiredKeyword\u001b[?25h" }, { "type": "promptInputChange", - "data": "[System.Xml.Linq.ReaderOptions|" + "data": "[Json.Schema.RequiredKeyword|" } ]; diff --git a/src/vs/workbench/contrib/terminalContrib/typeAhead/test/browser/terminalTypeAhead.test.ts b/src/vs/workbench/contrib/terminalContrib/typeAhead/test/browser/terminalTypeAhead.test.ts index 86972f65fa7..14f61b9868a 100644 --- a/src/vs/workbench/contrib/terminalContrib/typeAhead/test/browser/terminalTypeAhead.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/typeAhead/test/browser/terminalTypeAhead.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import type { IBuffer, Terminal } from '@xterm/xterm'; import { SinonStub, stub, useFakeTimers } from 'sinon'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index 67cc55a485d..61806a01f25 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -16,7 +16,7 @@ import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { autorun, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; +import { autorun, derived, observableFromEvent } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; import { isUriComponents, URI } from 'vs/base/common/uri'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; @@ -51,7 +51,6 @@ import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { CoverageDetails, DetailType, IDeclarationCoverage, IStatementCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; -const MAX_HOVERED_LINES = 30; const CLASS_HIT = 'coverage-deco-hit'; const CLASS_MISS = 'coverage-deco-miss'; const TOGGLE_INLINE_COMMAND_TEXT = localize('testing.toggleInlineCoverage', 'Toggle Inline'); @@ -59,9 +58,6 @@ const TOGGLE_INLINE_COMMAND_ID = 'testing.toggleInlineCoverage'; const BRANCH_MISS_INDICATOR_CHARS = 4; export class CodeCoverageDecorations extends Disposable implements IEditorContribution { - public static showInline = observableValue('inlineCoverage', false); - private static readonly fileCoverageDecorations = new WeakMap(); - private loadingCancellation?: CancellationTokenSource; private readonly displayedStore = this._register(new DisposableStore()); private readonly hoveredStore = this._register(new DisposableStore()); @@ -77,7 +73,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri constructor( private readonly editor: ICodeEditor, @IInstantiationService instantiationService: IInstantiationService, - @ITestCoverageService coverage: ITestCoverageService, + @ITestCoverageService private readonly coverage: ITestCoverageService, @IConfigurationService configurationService: IConfigurationService, @ILogService private readonly log: ILogService, ) { @@ -85,8 +81,8 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri this.summaryWidget = new Lazy(() => this._register(instantiationService.createInstance(CoverageToolbarWidget, this.editor))); - const modelObs = observableFromEvent(editor.onDidChangeModel, () => editor.getModel()); - const configObs = observableFromEvent(editor.onDidChangeConfiguration, i => i); + const modelObs = observableFromEvent(this, editor.onDidChangeModel, () => editor.getModel()); + const configObs = observableFromEvent(this, editor.onDidChangeConfiguration, i => i); const fileCoverage = derived(reader => { const report = coverage.selected.read(reader); @@ -99,24 +95,19 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri return; } - let file = report.getUri(model.uri); - if (file) { - const testFilter = coverage.filterToTest.read(reader); - if (testFilter) { - file = file.perTestData?.get(testFilter.toString()) || file; - } - - return file; + const file = report.getUri(model.uri); + if (!file) { + return; } report.didAddCoverage.read(reader); // re-read if changes when there's no report - return undefined; + return { file, testId: coverage.filterToTest.read(reader) }; }); this._register(autorun(reader => { const c = fileCoverage.read(reader); if (c) { - this.apply(editor.getModel()!, c, CodeCoverageDecorations.showInline.read(reader)); + this.apply(editor.getModel()!, c.file, c.testId, coverage.showInline.read(reader)); } else { this.clear(); } @@ -126,9 +117,9 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri this._register(autorun(reader => { const c = fileCoverage.read(reader); if (c && toolbarEnabled.read(reader)) { - this.summaryWidget.value.setCoverage(c); + this.summaryWidget.value.setCoverage(c.file, c.testId); } else { - this.summaryWidget.rawValue?.setCoverage(undefined); + this.summaryWidget.rawValue?.clearCoverage(); } })); @@ -146,7 +137,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri const model = editor.getModel(); if (e.target.type === MouseTargetType.GUTTER_LINE_NUMBERS && model) { this.hoverLineNumber(editor.getModel()!, e.target.position.lineNumber); - } else if (CodeCoverageDecorations.showInline.get() && e.target.type === MouseTargetType.CONTENT_TEXT && model) { + } else if (coverage.showInline.get() && e.target.type === MouseTargetType.CONTENT_TEXT && model) { this.hoverInlineDecoration(model, e.target.position); } else { this.hoveredStore.clear(); @@ -214,9 +205,14 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri const todo = [{ line: lineNumber, dir: 0 }]; const toEnable = new Set(); - if (!CodeCoverageDecorations.showInline.get()) { - for (let i = 0; i < todo.length && i < MAX_HOVERED_LINES; i++) { + const ranges = this.editor.getVisibleRanges(); + if (!this.coverage.showInline.get()) { + for (let i = 0; i < todo.length; i++) { const { line, dir } = todo[i]; + if (!ranges.some(r => r.startLineNumber <= line && r.endLineNumber >= line)) { + continue; // stop once outside the viewport + } + let found = false; for (const decoration of model.getLineDecorations(line)) { if (this.decorationIds.has(decoration.id)) { @@ -262,8 +258,8 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri })); } - private async apply(model: ITextModel, coverage: FileCoverage, showInlineByDefault: boolean) { - const details = this.details = await this.loadDetails(coverage, model); + private async apply(model: ITextModel, coverage: FileCoverage, testId: TestId | undefined, showInlineByDefault: boolean) { + const details = this.details = await this.loadDetails(coverage, testId, model); if (!details) { return this.clear(); } @@ -348,24 +344,18 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri this.hoveredStore.clear(); } - private async loadDetails(coverage: FileCoverage, textModel: ITextModel) { - const existing = CodeCoverageDecorations.fileCoverageDecorations.get(coverage); - if (existing) { - return existing; - } - + private async loadDetails(coverage: FileCoverage, testId: TestId | undefined, textModel: ITextModel) { const cts = this.loadingCancellation = new CancellationTokenSource(); this.displayedStore.add(this.loadingCancellation); try { - const details = await coverage.details(this.loadingCancellation.token); + const details = testId + ? await coverage.detailsForTest(testId, this.loadingCancellation.token) + : await coverage.details(this.loadingCancellation.token); if (cts.token.isCancellationRequested) { return; } - const model = CodeCoverageDecorations.fileCoverageDecorations.get(coverage) - || new CoverageDetailsModel(details, textModel); - CodeCoverageDecorations.fileCoverageDecorations.set(coverage, model); - return model; + return new CoverageDetailsModel(details, textModel); } catch (e) { this.log.error('Error loading coverage details', e); } @@ -535,7 +525,7 @@ function wrapName(functionNameOrCode: string) { } class CoverageToolbarWidget extends Disposable implements IOverlayWidget { - private current: FileCoverage | undefined; + private current: { coverage: FileCoverage; testId: TestId | undefined } | undefined; private registered = false; private isRunning = false; private readonly showStore = this._register(new DisposableStore()); @@ -556,6 +546,7 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { @ITestService private readonly testService: ITestService, @IKeybindingService private readonly keybindingService: IKeybindingService, @ICommandService private readonly commandService: ICommandService, + @ITestCoverageService private readonly coverage: ITestCoverageService, @IInstantiationService instaService: IInstantiationService, ) { super(); @@ -579,7 +570,7 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { this._register(autorun(reader => { - CodeCoverageDecorations.showInline.read(reader); + coverage.showInline.read(reader); this.setActions(); })); @@ -609,8 +600,14 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { }; } - public setCoverage(coverage: FileCoverage | undefined) { - this.current = coverage; + public clearCoverage() { + this.current = undefined; + this.bars.setCoverageInfo(undefined); + this.hide(); + } + + public setCoverage(coverage: FileCoverage, testId: TestId | undefined) { + this.current = { coverage, testId }; this.bars.setCoverageInfo(coverage); if (!coverage) { @@ -623,19 +620,19 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { private setActions() { this.actionBar.clear(); - const coverage = this.current; - if (!coverage) { + const current = this.current; + if (!current) { return; } const toggleAction = new ActionWithIcon( 'toggleInline', - CodeCoverageDecorations.showInline.get() + this.coverage.showInline.get() ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') : localize('testing.showInlineCoverage', 'Show Inline Coverage'), testingCoverageReport, undefined, - () => CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined), + () => this.coverage.showInline.set(!this.coverage.showInline.get(), undefined), ); const kb = this.keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); @@ -645,21 +642,21 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { this.actionBar.push(toggleAction); - if (coverage.isForTest) { - const testItem = coverage.fromResult.getTestById(coverage.isForTest.id.toString()); + if (current.testId) { + const testItem = current.coverage.fromResult.getTestById(current.testId.toString()); assert(!!testItem, 'got coverage for an unreported test'); this.actionBar.push(new ActionWithIcon('perTestFilter', coverUtils.labels.showingFilterFor(testItem.label), testingFilterIcon, undefined, - () => this.commandService.executeCommand(TestCommandId.CoverageFilterToTestInEditor, this.current), + () => this.commandService.executeCommand(TestCommandId.CoverageFilterToTestInEditor, this.current, this.editor), )); - } else if (coverage.perTestData?.size) { + } else if (current.coverage.perTestData?.size) { this.actionBar.push(new ActionWithIcon('perTestFilter', - localize('testing.coverageForTestAvailable', "{0} test(s) ran code in this file", coverage.perTestData.size), + localize('testing.coverageForTestAvailable', "{0} test(s) ran code in this file", current.coverage.perTestData.size), testingFilterIcon, undefined, - () => this.commandService.executeCommand(TestCommandId.CoverageFilterToTestInEditor, this.current), + () => this.commandService.executeCommand(TestCommandId.CoverageFilterToTestInEditor, this.current, this.editor), )); } @@ -701,8 +698,8 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { })); ds.add(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TestingConfigKeys.CoverageBarThresholds) || e.affectsConfiguration(TestingConfigKeys.CoveragePercent)) { - this.setCoverage(this.current); + if (this.current && (e.affectsConfiguration(TestingConfigKeys.CoverageBarThresholds) || e.affectsConfiguration(TestingConfigKeys.CoveragePercent))) { + this.setCoverage(this.current.coverage, this.current.testId); } })); } @@ -712,7 +709,7 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { if (current) { this.isRunning = true; this.setActions(); - this.testService.runResolvedTests(current.fromResult.request).finally(() => { + this.testService.runResolvedTests(current.coverage.fromResult.request).finally(() => { this.isRunning = false; this.setActions(); }); @@ -728,12 +725,18 @@ registerAction2(class ToggleInlineCoverage extends Action2 { constructor() { super({ id: TOGGLE_INLINE_COMMAND_ID, - title: localize2('coverage.toggleInline', "Show Inline Coverage"), + // note: ideally this would be "show inline", but the command palette does + // not use the 'toggled' titles, so we need to make this generic. + title: localize2('coverage.toggleInline', "Toggle Inline Coverage"), category: Categories.Test, keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI), }, + toggled: { + condition: TestingContextKeys.inlineCoverageEnabled, + title: localize('coverage.hideInline', "Hide Inline Coverage"), + }, icon: testingCoverageReport, menu: [ { id: MenuId.CommandPalette, when: TestingContextKeys.isTestCoverageOpen }, @@ -742,8 +745,9 @@ registerAction2(class ToggleInlineCoverage extends Action2 { }); } - public run() { - CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined); + public run(accessor: ServicesAccessor): void { + const coverage = accessor.get(ITestCoverageService); + coverage.showInline.set(!coverage.showInline.get(), undefined); } }); @@ -791,10 +795,10 @@ registerAction2(class FilterCoverageToTestInEditor extends Action2 { }); } - run(accessor: ServicesAccessor, coverageOrUri?: FileCoverage | URI): void { + run(accessor: ServicesAccessor, coverageOrUri?: FileCoverage | URI, editor?: ICodeEditor): void { const testCoverageService = accessor.get(ITestCoverageService); const quickInputService = accessor.get(IQuickInputService); - const activeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor(); + const activeEditor = editor ?? accessor.get(ICodeEditorService).getActiveCodeEditor(); let coverage: FileCoverage | undefined; if (coverageOrUri instanceof FileCoverage) { coverage = coverageOrUri; @@ -805,26 +809,21 @@ registerAction2(class FilterCoverageToTestInEditor extends Action2 { coverage = uri && testCoverageService.selected.get()?.getUri(uri); } - if (!coverage || !(coverage.isForTest || coverage.perTestData?.size)) { - return; - } - - const options = coverage?.perTestData ?? coverage?.isForTest?.parent.perTestData; - if (!options) { + if (!coverage || !coverage.perTestData?.size) { return; } - const tests = [...options.values()]; - const commonPrefix = TestId.getLengthOfCommonPrefix(tests.length, i => tests[i].isForTest!.id); + const tests = [...coverage.perTestData].map(TestId.fromString); + const commonPrefix = TestId.getLengthOfCommonPrefix(tests.length, i => tests[i]); const result = coverage.fromResult; const previousSelection = testCoverageService.filterToTest.get(); - type TItem = { label: string; description?: string; item: FileCoverage | undefined }; + type TItem = { label: string; testId: TestId | undefined }; const items: QuickPickInput[] = [ - { label: coverUtils.labels.allTests, item: undefined }, + { label: coverUtils.labels.allTests, testId: undefined }, { type: 'separator' }, - ...tests.map(item => ({ label: coverUtils.getLabelForItem(result, item.isForTest!.id, commonPrefix), description: coverUtils.labels.percentCoverage(item.tpc), item })), + ...tests.map(id => ({ label: coverUtils.getLabelForItem(result, id, commonPrefix), testId: id })), ]; // These handle the behavior that reveals the start of coverage when the @@ -837,13 +836,13 @@ registerAction2(class FilterCoverageToTestInEditor extends Action2 { activeItem: items.find((item): item is TItem => 'item' in item && item.item === coverage), placeHolder: coverUtils.labels.pickShowCoverage, onDidFocus: (entry) => { - if (!entry.item) { + if (!entry.testId) { revealScrollCts.clear(); activeEditor?.setScrollTop(scrollTop); testCoverageService.filterToTest.set(undefined, undefined); } else { const cts = revealScrollCts.value = new CancellationTokenSource(); - entry.item.details(cts.token).then( + coverage.detailsForTest(entry.testId, cts.token).then( details => { const first = details.find(d => d.type === DetailType.Statement); if (!cts.token.isCancellationRequested && first) { @@ -852,7 +851,7 @@ registerAction2(class FilterCoverageToTestInEditor extends Action2 { }, () => { /* ignored */ } ); - testCoverageService.filterToTest.set(entry.item.isForTest!.id, undefined); + testCoverageService.filterToTest.set(entry.testId, undefined); } }, }).then(selected => { @@ -861,7 +860,7 @@ registerAction2(class FilterCoverageToTestInEditor extends Action2 { } revealScrollCts.dispose(); - testCoverageService.filterToTest.set(selected ? selected.item?.isForTest!.id : previousSelection, undefined); + testCoverageService.filterToTest.set(selected ? selected.testId : previousSelection, undefined); }); } }); diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts index 33060d117ea..9e752e5f4ef 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts @@ -223,9 +223,11 @@ export class TreeProjection extends Disposable implements ITestTreeProjection { break; } - // The first element will cause the root to be hidden + // Removing the first element will cause the root to be hidden. + // Changing first-level elements will need the root to re-render if + // there are no other controllers with items. const parent = toRemove.parent; - const affectsRootElement = toRemove.depth === 1 && parent?.children.size === 1; + const affectsRootElement = toRemove.depth === 1 && (parent?.children.size === 1 || !Iterable.some(this.rootsWithChildren, (_, i) => i === 1)); this.changedParents.add(affectsRootElement ? null : parent); const queue: Iterable[] = [[toRemove]]; diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts b/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts index 3e4cf9e7131..0c0111ec344 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { h } from 'vs/base/browser/dom'; -import type { IUpdatableHover, IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover, IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Lazy } from 'vs/base/common/lazy'; @@ -67,7 +67,7 @@ export class ManagedTestCoverageBars extends Disposable { }); private readonly visibleStore = this._register(new DisposableStore()); - private readonly customHovers: IUpdatableHover[] = []; + private readonly customHovers: IManagedHover[] = []; /** Gets whether coverage is currently visible for the resource. */ public get visible() { @@ -82,8 +82,8 @@ export class ManagedTestCoverageBars extends Disposable { super(); } - private attachHover(target: HTMLElement, factory: (coverage: CoverageBarSource) => string | IUpdatableHoverTooltipMarkdownString | undefined) { - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), target, () => this._coverage && factory(this._coverage))); + private attachHover(target: HTMLElement, factory: (coverage: CoverageBarSource) => string | IManagedHoverTooltipMarkdownString | undefined) { + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), target, () => this._coverage && factory(this._coverage))); } public setCoverageInfo(coverage: CoverageBarSource | undefined) { @@ -99,7 +99,7 @@ export class ManagedTestCoverageBars extends Disposable { if (!this._coverage) { const root = this.el.value.root; - ds.add(toDisposable(() => this.options.container.removeChild(root))); + ds.add(toDisposable(() => root.remove())); this.options.container.appendChild(root); ds.add(this.configurationService.onDidChangeConfiguration(c => { if (!this._coverage) { @@ -165,7 +165,7 @@ const stmtCoverageText = (coverage: CoverageBarSource) => localize('statementCov const fnCoverageText = (coverage: CoverageBarSource) => coverage.declaration && localize('functionCoverage', '{0}/{1} functions covered ({2})', nf.format(coverage.declaration.covered), nf.format(coverage.declaration.total), coverUtils.displayPercent(coverUtils.percent(coverage.declaration))); const branchCoverageText = (coverage: CoverageBarSource) => coverage.branch && localize('branchCoverage', '{0}/{1} branches covered ({2})', nf.format(coverage.branch.covered), nf.format(coverage.branch.total), coverUtils.displayPercent(coverUtils.percent(coverage.branch))); -const getOverallHoverText = (coverage: CoverageBarSource): IUpdatableHoverTooltipMarkdownString => { +const getOverallHoverText = (coverage: CoverageBarSource): IManagedHoverTooltipMarkdownString => { const str = [ stmtCoverageText(coverage), fnCoverageText(coverage), diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts index 8e67eebd282..26ecefd409a 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts @@ -643,7 +643,7 @@ registerAction2(class TestCoverageChangePerTestFilterAction extends Action2 { return; } - const tests = [...coverage.perTestCoverageIDs].map(TestId.fromString); + const tests = [...coverage.allPerTestIDs()].map(TestId.fromString); const commonPrefix = TestId.getLengthOfCommonPrefix(tests.length, i => tests[i]); const result = coverage.result; const previousSelection = coverageService.filterToTest.get(); diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 6851abd333b..37cb8843ff5 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -44,7 +44,7 @@ import { ITestProfileService, canUseProfileWithTest } from 'vs/workbench/contrib import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { IMainThreadTestCollection, IMainThreadTestController, ITestService, expandAndGetTestById, testsInFile, testsUnderUri } from 'vs/workbench/contrib/testing/common/testService'; -import { ExtTestRunProfileKind, ITestRunProfile, InternalTestItem, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; +import { ExtTestRunProfileKind, ITestRunProfile, InternalTestItem, TestItemExpandState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/common/testingContinuousRunService'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; @@ -224,8 +224,8 @@ export class RunUsingProfileAction extends Action2 { } testService.runResolvedTests({ + group: profile.group, targets: [{ - profileGroup: profile.group, profileId: profile.profileId, controllerId: profile.controllerId, testIds: elements.filter(t => canUseProfileWithTest(profile, t.test)).map(t => t.test.item.extId) @@ -625,7 +625,8 @@ abstract class RunOrDebugAllTestsAction extends Action2 { const testService = accessor.get(ITestService); const notifications = accessor.get(INotificationService); - const roots = [...testService.collection.rootItems]; + const roots = [...testService.collection.rootItems].filter(r => r.children.size + || r.expand === TestItemExpandState.Expandable || r.expand === TestItemExpandState.BusyExpanding); if (!roots.length) { notifications.info(this.noTestsFoundError); return; @@ -1345,7 +1346,8 @@ abstract class RunOrDebugFailedTests extends RunOrDebugExtsByPath { } } -abstract class RunOrDebugLastRun extends RunOrDebugExtsByPath { + +abstract class RunOrDebugLastRun extends Action2 { constructor(options: IAction2Options) { super({ ...options, @@ -1359,21 +1361,46 @@ abstract class RunOrDebugLastRun extends RunOrDebugExtsByPath { }); } - /** - * @inheritdoc - */ - protected *getTestExtIdsToRun(accessor: ServicesAccessor, runId?: string): Iterable { + protected abstract getGroup(): TestRunProfileBitset; + + protected getLastTestRunRequest(accessor: ServicesAccessor, runId?: string) { + const resultService = accessor.get(ITestResultService); + const lastResult = runId ? resultService.results.find(r => r.id === runId) : resultService.results[0]; + return lastResult?.request; + } + + /** @inheritdoc */ + public override async run(accessor: ServicesAccessor, runId?: string) { const resultService = accessor.get(ITestResultService); const lastResult = runId ? resultService.results.find(r => r.id === runId) : resultService.results[0]; if (!lastResult) { return; } - for (const test of lastResult.request.targets) { - for (const testId of test.testIds) { - yield testId; - } - } + const req = lastResult.request; + const testService = accessor.get(ITestService); + const profileService = accessor.get(ITestProfileService); + const profileExists = (t: { controllerId: string; profileId: number }) => + profileService.getControllerProfiles(t.controllerId).some(p => p.profileId === t.profileId); + + await discoverAndRunTests( + testService.collection, + accessor.get(IProgressService), + req.targets.flatMap(t => t.testIds), + tests => { + // If we're requesting a re-run in the same group and have the same profiles + // as were used before, then use those exactly. Otherwise guess naively. + if (this.getGroup() & req.group && req.targets.every(profileExists)) { + return testService.runResolvedTests({ + targets: req.targets, + group: req.group, + exclude: req.exclude, + }); + } else { + return testService.runTests({ tests, group: this.getGroup() }); + } + }, + ); } } @@ -1432,11 +1459,8 @@ export class ReRunLastRun extends RunOrDebugLastRun { }); } - protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise { - return service.runTests({ - group: TestRunProfileBitset.Run, - tests: internalTests, - }); + protected override getGroup(): TestRunProfileBitset { + return TestRunProfileBitset.Run; } } @@ -1453,11 +1477,8 @@ export class DebugLastRun extends RunOrDebugLastRun { }); } - protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise { - return service.runTests({ - group: TestRunProfileBitset.Debug, - tests: internalTests, - }); + protected override getGroup(): TestRunProfileBitset { + return TestRunProfileBitset.Debug; } } @@ -1474,11 +1495,8 @@ export class CoverageLastRun extends RunOrDebugLastRun { }); } - protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise { - return service.runTests({ - group: TestRunProfileBitset.Coverage, - tests: internalTests, - }); + protected override getGroup(): TestRunProfileBitset { + return TestRunProfileBitset.Coverage; } } diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index b2742e2480d..9afbdafa192 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -856,8 +856,8 @@ abstract class RunTestDecoration { } this.testService.runResolvedTests({ + group: profile.group, targets: [{ - profileGroup: profile.group, profileId: profile.profileId, controllerId: profile.controllerId, testIds: [test.item.extId] diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index 6f90138b102..b4a2281c215 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -28,6 +28,7 @@ const testFilterDescriptions: { [K in TestFilterTerm]: string } = { [TestFilterTerm.Failed]: localize('testing.filters.showOnlyFailed', "Show Only Failed Tests"), [TestFilterTerm.Executed]: localize('testing.filters.showOnlyExecuted', "Show Only Executed Tests"), [TestFilterTerm.CurrentDoc]: localize('testing.filters.currentFile', "Show in Active File Only"), + [TestFilterTerm.OpenedFiles]: localize('testing.filters.openedFiles', "Show in Opened Files Only"), [TestFilterTerm.Hidden]: localize('testing.filters.showExcludedTests', "Show Hidden Tests"), }; @@ -201,7 +202,7 @@ class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { private getActions(): IAction[] { return [ - ...[TestFilterTerm.Failed, TestFilterTerm.Executed, TestFilterTerm.CurrentDoc].map(term => ({ + ...[TestFilterTerm.Failed, TestFilterTerm.Executed, TestFilterTerm.CurrentDoc, TestFilterTerm.OpenedFiles].map(term => ({ checked: this.filters.isFilteringFor(term), class: undefined, enabled: true, diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 5c5572d1dc0..31f7be62310 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -8,7 +8,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { ActionBar, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button } from 'vs/base/browser/ui/button/button'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; @@ -22,6 +22,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { autorun, observableFromEvent } from 'vs/base/common/observable'; import { fuzzyContains } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { isDefined } from 'vs/base/common/types'; @@ -78,6 +79,7 @@ import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/commo import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { cmpPriority, isFailedState, isStateWithResult, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates'; import { IActivityService, IconBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; const enum LastFocusState { @@ -337,8 +339,8 @@ export class TestingExplorerView extends ViewPane { const { include, exclude } = this.getTreeIncludeExclude(undefined, profile); this.testService.runResolvedTests({ exclude: exclude.map(e => e.item.extId), + group: profile.group, targets: [{ - profileGroup: profile.group, profileId: profile.profileId, controllerId: profile.controllerId, testIds: include.map(i => i.item.extId), @@ -448,7 +450,7 @@ class ResultSummaryView extends Disposable { private elementsWereAttached = false; private badgeType: TestingCountBadge; private lastBadge?: NumberBadge | IconBadge; - private countHover: IUpdatableHover; + private countHover: IManagedHover; private readonly badgeDisposable = this._register(new MutableDisposable()); private readonly renderLoop = this._register(new RunOnceScheduler(() => this.render(), SUMMARY_RENDER_INTERVAL)); private readonly elements = dom.h('div.result-summary', [ @@ -480,7 +482,7 @@ class ResultSummaryView extends Disposable { } })); - this.countHover = this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.elements.count, '')); + this.countHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.elements.count, '')); const ab = this._register(new ActionBar(this.elements.rerun, { actionViewItemProvider: (action, options) => createActionViewItem(instantiationService, action, options), @@ -500,7 +502,7 @@ class ResultSummaryView extends Disposable { const { count, root, status, duration, rerun } = this.elements; if (!results.length) { if (this.elementsWereAttached) { - this.container.removeChild(root); + root.remove(); this.elementsWereAttached = false; } this.container.innerText = localize('noResults', 'No test results yet.'); @@ -648,6 +650,7 @@ class TestingExplorerViewModel extends Disposable { onDidChangeVisibility: Event, @IConfigurationService configurationService: IConfigurationService, @IEditorService editorService: IEditorService, + @IEditorGroupsService editorGroupsService: IEditorGroupsService, @IMenuService private readonly menuService: IMenuService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @ITestService private readonly testService: ITestService, @@ -818,27 +821,38 @@ class TestingExplorerViewModel extends Disposable { this.tree.rerender(); })); - const onEditorChange = () => { + const allOpenEditorInputs = observableFromEvent(this, + editorService.onDidEditorsChange, + () => new Set(editorGroupsService.groups.flatMap(g => g.editors).map(e => e.resource).filter(isDefined)), + ); + + const activeResource = observableFromEvent(this, editorService.onDidActiveEditorChange, () => { if (editorService.activeEditor instanceof DiffEditorInput) { - this.filter.filterToDocumentUri(editorService.activeEditor.primary.resource); + return editorService.activeEditor.primary.resource; } else { - this.filter.filterToDocumentUri(editorService.activeEditor?.resource); + return editorService.activeEditor?.resource; } + }); - if (this.filterState.isFilteringFor(TestFilterTerm.CurrentDoc)) { - this.tree.refilter(); + const filterText = observableFromEvent(this.filterState.text.onDidChange, () => this.filterState.text); + this._register(autorun(reader => { + filterText.read(reader); + if (this.filterState.isFilteringFor(TestFilterTerm.OpenedFiles)) { + this.filter.filterToDocumentUri([...allOpenEditorInputs.read(reader)]); + } else { + this.filter.filterToDocumentUri([activeResource.read(reader)].filter(isDefined)); } - }; - this._register(editorService.onDidActiveEditorChange(onEditorChange)); + if (this.filterState.isFilteringFor(TestFilterTerm.CurrentDoc) || this.filterState.isFilteringFor(TestFilterTerm.OpenedFiles)) { + this.tree.refilter(); + } + })); this._register(this.storageService.onWillSaveState(({ reason, }) => { if (reason === WillSaveStateReason.SHUTDOWN) { this.lastViewState.store(this.tree.getOptimizedViewState()); } })); - - onEditorChange(); } /** @@ -1067,7 +1081,7 @@ const hasNodeInOrParentOfUri = (collection: IMainThreadTestCollection, ident: IU }; class TestsFilter implements ITreeFilter { - private documentUri: URI | undefined; + private documentUris: URI[] = []; constructor( private readonly collection: IMainThreadTestCollection, @@ -1102,8 +1116,8 @@ class TestsFilter implements ITreeFilter { } } - public filterToDocumentUri(uri: URI | undefined) { - this.documentUri = uri; + public filterToDocumentUri(uris: readonly URI[]) { + this.documentUris = [...uris]; } private testTags(element: TestItemTreeElement): FilterResult { @@ -1131,15 +1145,15 @@ class TestsFilter implements ITreeFilter { } private testLocation(element: TestItemTreeElement): FilterResult { - if (!this.documentUri) { + if (this.documentUris.length === 0) { return FilterResult.Include; } - if (!this.state.isFilteringFor(TestFilterTerm.CurrentDoc) || !(element instanceof TestItemTreeElement)) { + if ((!this.state.isFilteringFor(TestFilterTerm.CurrentDoc) && !this.state.isFilteringFor(TestFilterTerm.OpenedFiles)) || !(element instanceof TestItemTreeElement)) { return FilterResult.Include; } - if (hasNodeInOrParentOfUri(this.collection, this.uriIdentityService, this.documentUri, element.test.item.extId)) { + if (this.documentUris.some(uri => hasNodeInOrParentOfUri(this.collection, this.uriIdentityService, uri, element.test.item.extId))) { return FilterResult.Include; } @@ -1344,7 +1358,7 @@ class ErrorRenderer implements ITreeRenderer { - this.el.root.parentElement?.removeChild(this.el.root); + this.el.root.remove(); })); } @@ -871,6 +872,10 @@ class FollowupActionWidget extends Disposable { if (link.ariaDisabled !== 'true') { link.ariaDisabled = 'true'; fu.execute(); + + if (this.editor) { + TestingOutputPeekController.get(this.editor)?.removePeek(); + } } } } @@ -917,7 +922,7 @@ class TestResultsViewContent extends Disposable { const { historyVisible, showRevealLocationOnMessages } = this.options; const isInPeekView = this.editor !== undefined; const messageContainer = this.messageContainer = dom.append(containerElement, dom.$('.test-output-peek-message-container')); - this.followupWidget = this._register(this.instantiationService.createInstance(FollowupActionWidget, messageContainer)); + this.followupWidget = this._register(this.instantiationService.createInstance(FollowupActionWidget, messageContainer, this.editor)); this.contentProviders = [ this._register(this.instantiationService.createInstance(DiffContentProvider, this.editor, messageContainer)), this._register(this.instantiationService.createInstance(MarkdownTestMessagePeek, messageContainer)), @@ -1020,15 +1025,14 @@ class TestResultsViewContent extends Disposable { } - this.currentSubjectStore.add( - this.instantiationService - .createChild(new ServiceCollection([IContextKeyService, this.messageContextKeyService])) - .createInstance(FloatingClickMenu, { - container: this.messageContainer, - menuId: MenuId.TestMessageContent, - getActionArg: () => (subject as MessageSubject).context, - }) - ); + const instaService = this.currentSubjectStore.add(this.instantiationService + .createChild(new ServiceCollection([IContextKeyService, this.messageContextKeyService]))); + + this.currentSubjectStore.add(instaService.createInstance(FloatingClickMenu, { + container: this.messageContainer, + menuId: MenuId.TestMessageContent, + getActionArg: () => (subject as MessageSubject).context, + })); } public onLayoutBody(height: number, width: number) { @@ -1086,7 +1090,7 @@ class TestResultsPeek extends PeekViewWidget { if (!this.scopedContextKeyService) { this.scopedContextKeyService = this._disposables.add(this.contextKeyService.createScoped(container)); TestingContextKeys.isInPeek.bindTo(this.scopedContextKeyService).set(true); - const instaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); + const instaService = this._disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); this.content = this._disposables.add(instaService.createInstance(TestResultsViewContent, this.editor, { historyVisible: this.testingPeek.historyVisible, showRevealLocationOnMessages: false, locationForProgress: Testing.ResultsViewId })); } @@ -1377,7 +1381,7 @@ class ScrollableMarkdownMessage extends Disposable { container.appendChild(this.scrollable.getDomNode()); this._register(toDisposable(() => { - container.removeChild(this.scrollable.getDomNode()); + this.scrollable.getDomNode().remove(); })); this.scrollable.scanDomNode(); @@ -2057,6 +2061,7 @@ class OutputPeekTree extends Disposable { result = Iterable.concat( Iterable.single>({ element: new CoverageElement(results, task, coverageService), + collapsible: true, incompressible: true, }), result, @@ -2082,6 +2087,7 @@ class OutputPeekTree extends Disposable { return ({ element: taskElem, incompressible: false, + collapsible: true, children: getTaskChildren(taskElem), }); }); @@ -2092,6 +2098,7 @@ class OutputPeekTree extends Disposable { return { element, incompressible: true, + collapsible: true, collapsed: this.tree.hasElement(element) ? this.tree.isCollapsed(element) : true, children: getResultChildren(result) }; diff --git a/src/vs/workbench/contrib/testing/browser/theme.ts b/src/vs/workbench/contrib/testing/browser/theme.ts index 536c03da5f9..0c088a787c3 100644 --- a/src/vs/workbench/contrib/testing/browser/theme.ts +++ b/src/vs/workbench/contrib/testing/browser/theme.ts @@ -30,33 +30,13 @@ export const testingColorIconPassed = registerColor('testing.iconPassed', { hcLight: '#007100' }, localize('testing.iconPassed', "Color for the 'passed' icon in the test explorer.")); -export const testingColorRunAction = registerColor('testing.runAction', { - dark: testingColorIconPassed, - light: testingColorIconPassed, - hcDark: testingColorIconPassed, - hcLight: testingColorIconPassed -}, localize('testing.runAction', "Color for 'run' icons in the editor.")); - -export const testingColorIconQueued = registerColor('testing.iconQueued', { - dark: '#cca700', - light: '#cca700', - hcDark: '#cca700', - hcLight: '#cca700' -}, localize('testing.iconQueued', "Color for the 'Queued' icon in the test explorer.")); - -export const testingColorIconUnset = registerColor('testing.iconUnset', { - dark: '#848484', - light: '#848484', - hcDark: '#848484', - hcLight: '#848484' -}, localize('testing.iconUnset', "Color for the 'Unset' icon in the test explorer.")); - -export const testingColorIconSkipped = registerColor('testing.iconSkipped', { - dark: '#848484', - light: '#848484', - hcDark: '#848484', - hcLight: '#848484' -}, localize('testing.iconSkipped', "Color for the 'Skipped' icon in the test explorer.")); +export const testingColorRunAction = registerColor('testing.runAction', testingColorIconPassed, localize('testing.runAction', "Color for 'run' icons in the editor.")); + +export const testingColorIconQueued = registerColor('testing.iconQueued', '#cca700', localize('testing.iconQueued', "Color for the 'Queued' icon in the test explorer.")); + +export const testingColorIconUnset = registerColor('testing.iconUnset', '#848484', localize('testing.iconUnset', "Color for the 'Unset' icon in the test explorer.")); + +export const testingColorIconSkipped = registerColor('testing.iconSkipped', '#848484', localize('testing.iconSkipped', "Color for the 'Skipped' icon in the test explorer.")); export const testingPeekBorder = registerColor('testing.peekBorder', { dark: editorErrorForeground, @@ -135,19 +115,9 @@ export const testingUncoveredGutterBackground = registerColor('testing.uncovered hcLight: chartsRed }, localize('testing.uncoveredGutterBackground', 'Gutter color of regions where code not covered.')); -export const testingCoverCountBadgeBackground = registerColor('testing.coverCountBadgeBackground', { - dark: badgeBackground, - light: badgeBackground, - hcDark: badgeBackground, - hcLight: badgeBackground -}, localize('testing.coverCountBadgeBackground', 'Background for the badge indicating execution count')); +export const testingCoverCountBadgeBackground = registerColor('testing.coverCountBadgeBackground', badgeBackground, localize('testing.coverCountBadgeBackground', 'Background for the badge indicating execution count')); -export const testingCoverCountBadgeForeground = registerColor('testing.coverCountBadgeForeground', { - dark: badgeForeground, - light: badgeForeground, - hcDark: badgeForeground, - hcLight: badgeForeground -}, localize('testing.coverCountBadgeForeground', 'Foreground for the badge indicating execution count')); +export const testingCoverCountBadgeForeground = registerColor('testing.coverCountBadgeForeground', badgeForeground, localize('testing.coverCountBadgeForeground', 'Foreground for the badge indicating execution count')); export const testMessageSeverityColors: { [K in TestMessageType]: { @@ -170,12 +140,12 @@ export const testMessageSeverityColors: { [TestMessageType.Output]: { decorationForeground: registerColor( 'testing.message.info.decorationForeground', - { dark: transparent(editorForeground, 0.5), light: transparent(editorForeground, 0.5), hcDark: transparent(editorForeground, 0.5), hcLight: transparent(editorForeground, 0.5) }, + transparent(editorForeground, 0.5), localize('testing.message.info.decorationForeground', 'Text color of test info messages shown inline in the editor.') ), marginBackground: registerColor( 'testing.message.info.lineBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, localize('testing.message.info.marginBackground', 'Margin color beside info messages shown inline in the editor.') ), }, @@ -190,47 +160,17 @@ export const testStatesToIconColors: { [K in TestResultState]?: string } = { [TestResultState.Skipped]: testingColorIconSkipped, }; -export const testingRetiredColorIconErrored = registerColor('testing.iconErrored.retired', { - dark: transparent(testingColorIconErrored, 0.7), - light: transparent(testingColorIconErrored, 0.7), - hcDark: transparent(testingColorIconErrored, 0.7), - hcLight: transparent(testingColorIconErrored, 0.7) -}, localize('testing.iconErrored.retired', "Retired color for the 'Errored' icon in the test explorer.")); - -export const testingRetiredColorIconFailed = registerColor('testing.iconFailed.retired', { - dark: transparent(testingColorIconFailed, 0.7), - light: transparent(testingColorIconFailed, 0.7), - hcDark: transparent(testingColorIconFailed, 0.7), - hcLight: transparent(testingColorIconFailed, 0.7) -}, localize('testing.iconFailed.retired', "Retired color for the 'failed' icon in the test explorer.")); - -export const testingRetiredColorIconPassed = registerColor('testing.iconPassed.retired', { - dark: transparent(testingColorIconPassed, 0.7), - light: transparent(testingColorIconPassed, 0.7), - hcDark: transparent(testingColorIconPassed, 0.7), - hcLight: transparent(testingColorIconPassed, 0.7) -}, localize('testing.iconPassed.retired', "Retired color for the 'passed' icon in the test explorer.")); - -export const testingRetiredColorIconQueued = registerColor('testing.iconQueued.retired', { - dark: transparent(testingColorIconQueued, 0.7), - light: transparent(testingColorIconQueued, 0.7), - hcDark: transparent(testingColorIconQueued, 0.7), - hcLight: transparent(testingColorIconQueued, 0.7) -}, localize('testing.iconQueued.retired', "Retired color for the 'Queued' icon in the test explorer.")); - -export const testingRetiredColorIconUnset = registerColor('testing.iconUnset.retired', { - dark: transparent(testingColorIconUnset, 0.7), - light: transparent(testingColorIconUnset, 0.7), - hcDark: transparent(testingColorIconUnset, 0.7), - hcLight: transparent(testingColorIconUnset, 0.7) -}, localize('testing.iconUnset.retired', "Retired color for the 'Unset' icon in the test explorer.")); - -export const testingRetiredColorIconSkipped = registerColor('testing.iconSkipped.retired', { - dark: transparent(testingColorIconSkipped, 0.7), - light: transparent(testingColorIconSkipped, 0.7), - hcDark: transparent(testingColorIconSkipped, 0.7), - hcLight: transparent(testingColorIconSkipped, 0.7) -}, localize('testing.iconSkipped.retired', "Retired color for the 'Skipped' icon in the test explorer.")); +export const testingRetiredColorIconErrored = registerColor('testing.iconErrored.retired', transparent(testingColorIconErrored, 0.7), localize('testing.iconErrored.retired', "Retired color for the 'Errored' icon in the test explorer.")); + +export const testingRetiredColorIconFailed = registerColor('testing.iconFailed.retired', transparent(testingColorIconFailed, 0.7), localize('testing.iconFailed.retired', "Retired color for the 'failed' icon in the test explorer.")); + +export const testingRetiredColorIconPassed = registerColor('testing.iconPassed.retired', transparent(testingColorIconPassed, 0.7), localize('testing.iconPassed.retired', "Retired color for the 'passed' icon in the test explorer.")); + +export const testingRetiredColorIconQueued = registerColor('testing.iconQueued.retired', transparent(testingColorIconQueued, 0.7), localize('testing.iconQueued.retired', "Retired color for the 'Queued' icon in the test explorer.")); + +export const testingRetiredColorIconUnset = registerColor('testing.iconUnset.retired', transparent(testingColorIconUnset, 0.7), localize('testing.iconUnset.retired', "Retired color for the 'Unset' icon in the test explorer.")); + +export const testingRetiredColorIconSkipped = registerColor('testing.iconSkipped.retired', transparent(testingColorIconSkipped, 0.7), localize('testing.iconSkipped.retired', "Retired color for the 'Skipped' icon in the test explorer.")); export const testStatesToRetiredIconColors: { [K in TestResultState]?: string } = { [TestResultState.Errored]: testingRetiredColorIconErrored, diff --git a/src/vs/workbench/contrib/testing/common/configuration.ts b/src/vs/workbench/contrib/testing/common/configuration.ts index ec9e50d67f0..91ca1a2bd18 100644 --- a/src/vs/workbench/contrib/testing/common/configuration.ts +++ b/src/vs/workbench/contrib/testing/common/configuration.ts @@ -159,7 +159,7 @@ export const testingConfiguration: IConfigurationNode = { description: localize('testing.openTesting', "Controls when the testing view should open.") }, [TestingConfigKeys.AlwaysRevealTestOnStateChange]: { - markdownDescription: localize('testing.alwaysRevealTestOnStateChange', "Always reveal the executed test when `#testing.followRunningTest#` is on. If this setting is turned off, only failed tests will be revealed."), + markdownDescription: localize('testing.alwaysRevealTestOnStateChange', "Always reveal the executed test when {0} is on. If this setting is turned off, only failed tests will be revealed.", '`#testing.followRunningTest#`'), type: 'boolean', default: false, }, diff --git a/src/vs/workbench/contrib/testing/common/testCoverage.ts b/src/vs/workbench/contrib/testing/common/testCoverage.ts index 321434bd602..aae8f0af362 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverage.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverage.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { assert } from 'vs/base/common/assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ResourceMap } from 'vs/base/common/map'; import { deepClone } from 'vs/base/common/objects'; @@ -13,10 +12,10 @@ import { URI } from 'vs/base/common/uri'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; -import { CoverageDetails, ICoverageCount, IFileCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; +import { CoverageDetails, DetailType, ICoverageCount, IFileCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; export interface ICoverageAccessor { - getCoverageDetails: (id: string, token: CancellationToken) => Promise; + getCoverageDetails: (id: string, testId: string | undefined, token: CancellationToken) => Promise; } let incId = 0; @@ -30,9 +29,6 @@ export class TestCoverage { public readonly tree = new WellDefinedPrefixTree(); public readonly associatedData = new Map(); - /** Test IDs that have per-test coverage in this output. */ - public readonly perTestCoverageIDs = new Set(); - constructor( public readonly result: LiveTestResult, public readonly fromTaskId: string, @@ -40,6 +36,21 @@ export class TestCoverage { private readonly accessor: ICoverageAccessor, ) { } + /** Gets all test IDs that were included in this test run. */ + public *allPerTestIDs() { + const seen = new Set(); + for (const root of this.tree.nodes) { + if (root.value && root.value.perTestData) { + for (const id of root.value.perTestData) { + if (!seen.has(id)) { + seen.add(id); + yield id; + } + } + } + } + } + public append(coverage: IFileCoverage, tx: ITransaction | undefined) { const previous = this.getComputedForUri(coverage.uri); const result = this.result; @@ -59,24 +70,13 @@ export class TestCoverage { // version. const canonical = [...this.treePathForUri(coverage.uri, /* canonical = */ true)]; const chain: IPrefixTreeNode[] = []; - const isPerTestCoverage = !!coverage.testId; - if (coverage.testId) { - this.perTestCoverageIDs.add(coverage.testId.toString()); - } + this.tree.mutatePath(this.treePathForUri(coverage.uri, /* canonical = */ false), node => { chain.push(node); if (chain.length === canonical.length) { // we reached our destination node, apply the coverage as necessary: - if (isPerTestCoverage) { - const v = node.value ??= new FileCoverage(IFileCoverage.empty(String(incId++), coverage.uri), result, this.accessor); - assert(v instanceof FileCoverage, 'coverage is unexpectedly computed'); - v.perTestData ??= new Map(); - const perTest = new FileCoverage(coverage, result, this.accessor); - perTest.isForTest = { id: coverage.testId!, parent: v }; - v.perTestData.set(coverage.testId!.toString(), perTest); - this.fileCoverage.set(coverage.uri, v); - } else if (node.value) { + if (node.value) { const v = node.value; // if ID was generated from a test-specific coverage, reassign it to get its real ID in the extension host. v.id = coverage.id; @@ -87,7 +87,7 @@ export class TestCoverage { const v = node.value = new FileCoverage(coverage, result, this.accessor); this.fileCoverage.set(coverage.uri, v); } - } else if (!isPerTestCoverage) { + } else { // Otherwise, if this is not a partial per-test coverage, merge the // coverage changes into the chain. Per-test coverages are not complete // and we don't want to consider them for computation. @@ -104,9 +104,16 @@ export class TestCoverage { node.value.didChange.trigger(tx); } } + + if (coverage.testIds) { + node.value!.perTestData ??= new Set(); + for (const id of coverage.testIds) { + node.value!.perTestData.add(id); + } + } }); - if (chain && !isPerTestCoverage) { + if (chain) { this.didAddCoverage.trigger(tx, chain); } } @@ -118,21 +125,15 @@ export class TestCoverage { const tree = new WellDefinedPrefixTree(); for (const node of this.tree.values()) { if (node instanceof FileCoverage) { - const fileData = node.perTestData?.get(testId.toString()); - if (!fileData) { + if (!node.perTestData?.has(testId.toString())) { continue; } - const canonical = [...this.treePathForUri(fileData.uri, /* canonical = */ true)]; + const canonical = [...this.treePathForUri(node.uri, /* canonical = */ true)]; const chain: IPrefixTreeNode[] = []; - tree.mutatePath(this.treePathForUri(fileData.uri, /* canonical = */ false), node => { - chain.push(node); - - if (chain.length === canonical.length) { - node.value = fileData; - } else { - node.value ??= new BypassedFileCoverage(this.treePathToUri(canonical.slice(0, chain.length)), fileData.fromResult); - } + tree.mutatePath(this.treePathForUri(node.uri, /* canonical = */ false), n => { + chain.push(n); + n.value ??= new BypassedFileCoverage(this.treePathToUri(canonical.slice(0, chain.length)), node.fromResult); }); } } @@ -208,6 +209,11 @@ export abstract class AbstractFileCoverage { return getTotalCoveragePercent(this.statement, this.branch, this.declaration); } + /** + * Per-test coverage data for this file, if available. + */ + public perTestData?: Set; + constructor(coverage: IFileCoverage, public readonly fromResult: LiveTestResult) { this.id = coverage.id; this.uri = coverage.uri; @@ -235,31 +241,46 @@ export class BypassedFileCoverage extends ComputedFileCoverage { export class FileCoverage extends AbstractFileCoverage { private _details?: Promise; private resolved?: boolean; + private _detailsForTest?: Map>; /** Gets whether details are synchronously available */ public get hasSynchronousDetails() { return this._details instanceof Array || this.resolved; } - /** - * Per-test coverage data for this file, if available. - */ - public perTestData?: Map; + constructor(coverage: IFileCoverage, fromResult: LiveTestResult, private readonly accessor: ICoverageAccessor) { + super(coverage, fromResult); + } /** - * If this is for a single test item, gets the test item. + * Gets per-line coverage details. */ - public isForTest?: { id: TestId; parent: FileCoverage }; + public async detailsForTest(_testId: TestId, token = CancellationToken.None) { + this._detailsForTest ??= new Map(); + const testId = _testId.toString(); + const prev = this._detailsForTest.get(testId); + if (prev) { + return prev; + } - constructor(coverage: IFileCoverage, fromResult: LiveTestResult, private readonly accessor: ICoverageAccessor) { - super(coverage, fromResult); + const promise = (async () => { + try { + return await this.accessor.getCoverageDetails(this.id, testId, token); + } catch (e) { + this._detailsForTest?.delete(testId); + throw e; + } + })(); + + this._detailsForTest.set(testId, promise); + return promise; } /** * Gets per-line coverage details. */ public async details(token = CancellationToken.None) { - this._details ??= this.accessor.getCoverageDetails(this.id, token); + this._details ??= this.accessor.getCoverageDetails(this.id, undefined, token); try { const d = await this._details; @@ -271,3 +292,30 @@ export class FileCoverage extends AbstractFileCoverage { } } } + +export const totalFromCoverageDetails = (uri: URI, details: CoverageDetails[]): IFileCoverage => { + const fc: IFileCoverage = { + id: '', + uri, + statement: ICoverageCount.empty(), + }; + + for (const detail of details) { + if (detail.type === DetailType.Statement) { + fc.statement.total++; + fc.statement.total += detail.count ? 1 : 0; + + for (const branch of detail.branches || []) { + fc.branch ??= ICoverageCount.empty(); + fc.branch.total++; + fc.branch.covered += branch.count ? 1 : 0; + } + } else { + fc.declaration ??= ICoverageCount.empty(); + fc.declaration.total++; + fc.declaration.covered += detail.count ? 1 : 0; + } + } + + return fc; +}; diff --git a/src/vs/workbench/contrib/testing/common/testCoverageService.ts b/src/vs/workbench/contrib/testing/common/testCoverageService.ts index e1b62d541dc..4433a713d2c 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverageService.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverageService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Iterable } from 'vs/base/common/iterator'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IObservable, ISettableObservable, observableValue, transaction } from 'vs/base/common/observable'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -35,6 +36,11 @@ export interface ITestCoverageService { */ readonly filterToTest: ISettableObservable; + /** + * Whether inline coverage is shown. + */ + readonly showInline: ISettableObservable; + /** * Opens a test coverage report from a task, optionally focusing it in the editor. */ @@ -52,6 +58,7 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ public readonly selected = observableValue('testCoverage', undefined); public readonly filterToTest = observableValue('filterToTest', undefined); + public readonly showInline = observableValue('inlineCoverage', false); constructor( @IContextKeyService contextKeyService: IContextKeyService, @@ -68,6 +75,12 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ reader => toolbarConfig.read(reader), )); + this._register(bindContextKey( + TestingContextKeys.inlineCoverageEnabled, + contextKeyService, + reader => this.showInline.read(reader), + )); + this._register(bindContextKey( TestingContextKeys.isTestCoverageOpen, contextKeyService, @@ -77,7 +90,7 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ this._register(bindContextKey( TestingContextKeys.hasPerTestCoverage, contextKeyService, - reader => !!this.selected.read(reader)?.perTestCoverageIDs.size, + reader => !Iterable.isEmpty(this.selected.read(reader)?.allPerTestIDs()), )); this._register(bindContextKey( diff --git a/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts index 3ab130d114f..e9c1275a79c 100644 --- a/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts +++ b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts @@ -199,6 +199,7 @@ export const enum TestFilterTerm { Failed = '@failed', Executed = '@executed', CurrentDoc = '@doc', + OpenedFiles = '@openedFiles', Hidden = '@hidden', } @@ -206,5 +207,6 @@ const allTestFilterTerms: readonly TestFilterTerm[] = [ TestFilterTerm.Failed, TestFilterTerm.Executed, TestFilterTerm.CurrentDoc, + TestFilterTerm.OpenedFiles, TestFilterTerm.Hidden, ]; diff --git a/src/vs/workbench/contrib/testing/common/testProfileService.ts b/src/vs/workbench/contrib/testing/common/testProfileService.ts index adf73855e6c..d01101c6919 100644 --- a/src/vs/workbench/contrib/testing/common/testProfileService.ts +++ b/src/vs/workbench/contrib/testing/common/testProfileService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; +import { Iterable } from 'vs/base/common/iterator'; import { Disposable } from 'vs/base/common/lifecycle'; import { deepClone } from 'vs/base/common/objects'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -64,7 +65,7 @@ export interface ITestProfileService { /** * Gets the default profiles to be run for a given run group. */ - getGroupDefaultProfiles(group: TestRunProfileBitset): ITestRunProfile[]; + getGroupDefaultProfiles(group: TestRunProfileBitset, controllerId?: string): ITestRunProfile[]; /** * Sets the default profiles to be run for a given run group. @@ -252,20 +253,17 @@ export class TestProfileService extends Disposable implements ITestProfileServic } /** @inheritdoc */ - public getGroupDefaultProfiles(group: TestRunProfileBitset) { - let defaults: ITestRunProfile[] = []; - for (const { profiles } of this.controllerProfiles.values()) { - defaults = defaults.concat(profiles.filter(c => c.group === group && c.isDefault)); - } + public getGroupDefaultProfiles(group: TestRunProfileBitset, controllerId?: string) { + const allProfiles = controllerId + ? (this.controllerProfiles.get(controllerId)?.profiles || []) + : [...Iterable.flatMap(this.controllerProfiles.values(), c => c.profiles)]; + const defaults = allProfiles.filter(c => c.group === group && c.isDefault); // have *some* default profile to run if none are set otherwise if (defaults.length === 0) { - for (const { profiles } of this.controllerProfiles.values()) { - const first = profiles.find(p => p.group === group); - if (first) { - defaults.push(first); - break; - } + const first = allProfiles.find(p => p.group === group); + if (first) { + defaults.push(first); } } diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index 3035be654df..88bdd37b5c2 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -16,7 +16,7 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { ITestResult, LiveTestResult, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage'; -import { ExtensionRunTestsRequest, ITestRunProfile, ResolvedTestRunRequest, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; +import { ExtensionRunTestsRequest, ITestRunProfile, ResolvedTestRunRequest, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; export type ResultChangeEvent = | { completed: LiveTestResult } @@ -153,11 +153,11 @@ export class TestResultService extends Disposable implements ITestResultService targets: [], exclude: req.exclude, continuous: req.continuous, + group: profile?.group ?? TestRunProfileBitset.Run, }; if (profile) { resolved.targets.push({ - profileGroup: profile.group, profileId: profile.profileId, controllerId: req.controllerId, testIds: req.include, diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index d3026db22eb..919fb4dbe1f 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -201,12 +201,9 @@ export const testsUnderUri = async function* (testService: ITestService, ident: // tests already encompass their children. if (!test) { // no-op - } else if (!test.item.uri) { - queue.push(test.children.values()); - continue; - } else if (ident.extUri.isEqualOrParent(test.item.uri, uri)) { + } else if (test.item.uri && ident.extUri.isEqualOrParent(test.item.uri, uri)) { yield test; - } else if (ident.extUri.isEqualOrParent(uri, test.item.uri)) { + } else if (!test.item.uri || ident.extUri.isEqualOrParent(uri, test.item.uri)) { if (test.expand === TestItemExpandState.Expandable) { await testService.collection.expand(test.item.extId, 1); } diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index fd3ffcd0999..7b0e47cd891 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -28,7 +28,7 @@ import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { AmbiguousRunTestsRequest, IMainThreadTestController, IMainThreadTestHostProxy, ITestFollowups, ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { ResolvedTestRunRequest, TestDiffOpType, TestMessageFollowupRequest, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; +import { InternalTestItem, ITestRunProfile, ResolvedTestRunRequest, TestDiffOpType, TestMessageFollowupRequest, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class TestService extends Disposable implements ITestService { @@ -133,25 +133,37 @@ export class TestService extends Disposable implements ITestService { * @inheritdoc */ public async runTests(req: AmbiguousRunTestsRequest, token = CancellationToken.None): Promise { + // We try to ensure that all tests in the request will be run, preferring + // to use default profiles for each controller when possible. + const byProfile: { profile: ITestRunProfile; tests: InternalTestItem[] }[] = []; + for (const test of req.tests) { + const existing = byProfile.find(p => canUseProfileWithTest(p.profile, test)); + if (existing) { + existing.tests.push(test); + continue; + } + + const allProfiles = this.testProfiles.getControllerProfiles(test.controllerId) + .filter(p => (p.group & req.group) !== 0 && canUseProfileWithTest(p, test)); + const bestProfile = allProfiles.find(p => p.isDefault) || allProfiles[0]; + if (!bestProfile) { + continue; + } + + byProfile.push({ profile: bestProfile, tests: [test] }); + } + const resolved: ResolvedTestRunRequest = { - targets: [], + targets: byProfile.map(({ profile, tests }) => ({ + profileId: profile.profileId, + controllerId: tests[0].controllerId, + testIds: tests.map(t => t.item.extId), + })), + group: req.group, exclude: req.exclude?.map(t => t.item.extId), continuous: req.continuous, }; - // First, try to run the tests using the default run profiles... - for (const profile of this.testProfiles.getGroupDefaultProfiles(req.group)) { - const testIds = req.tests.filter(t => canUseProfileWithTest(profile, t)).map(t => t.item.extId); - if (testIds.length) { - resolved.targets.push({ - testIds: testIds, - profileGroup: profile.group, - profileId: profile.profileId, - controllerId: profile.controllerId, - }); - } - } - // If no tests are covered by the defaults, just use whatever the defaults // for their controller are. This can happen if the user chose specific // profiles for the run button, but then asked to run a single test from the @@ -169,7 +181,6 @@ export class TestService extends Disposable implements ITestService { if (profile) { resolved.targets.push({ testIds: byProfile.map(t => t.test.item.extId), - profileGroup: req.group, profileId: profile.profileId, controllerId: profile.controllerId, }); diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index 75f7b371362..5a90948fc68 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -78,10 +78,10 @@ export interface ITestRunProfile { * and extension host. */ export interface ResolvedTestRunRequest { + group: TestRunProfileBitset; targets: { testIds: string[]; controllerId: string; - profileGroup: TestRunProfileBitset; profileId: number; }[]; exclude?: string[]; @@ -573,7 +573,7 @@ export namespace ICoverageCount { export interface IFileCoverage { id: string; uri: URI; - testId?: TestId; + testIds?: string[]; statement: ICoverageCount; branch?: ICoverageCount; declaration?: ICoverageCount; @@ -583,7 +583,7 @@ export namespace IFileCoverage { export interface Serialized { id: string; uri: UriComponents; - testId: string | undefined; + testIds: string[] | undefined; statement: ICoverageCount; branch?: ICoverageCount; declaration?: ICoverageCount; @@ -594,7 +594,7 @@ export namespace IFileCoverage { statement: original.statement, branch: original.branch, declaration: original.declaration, - testId: original.testId?.toString(), + testIds: original.testIds, uri: original.uri.toJSON(), }); @@ -603,14 +603,13 @@ export namespace IFileCoverage { statement: serialized.statement, branch: serialized.branch, declaration: serialized.declaration, - testId: serialized.testId ? TestId.fromString(serialized.testId) : undefined, + testIds: serialized.testIds, uri: uriIdentity.asCanonicalUri(URI.revive(serialized.uri)), }); export const empty = (id: string, uri: URI): IFileCoverage => ({ id, uri, - testId: undefined, statement: ICoverageCount.empty(), }); } diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index 96682ebf7c9..1463fb3c1d4 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -25,6 +25,7 @@ export namespace TestingContextKeys { export const hasPerTestCoverage = new RawContextKey('testing.hasPerTestCoverage', false, { type: 'boolean', description: localize('testing.hasPerTestCoverage', 'Indicates whether per-test coverage is available') }); export const isCoverageFilteredToTest = new RawContextKey('testing.isCoverageFilteredToTest', false, { type: 'boolean', description: localize('testing.isCoverageFilteredToTest', 'Indicates whether coverage has been filterd to a single test') }); export const coverageToolbarEnabled = new RawContextKey('testing.coverageToolbarEnabled', true, { type: 'boolean', description: localize('testing.coverageToolbarEnabled', 'Indicates whether the coverage toolbar is enabled') }); + export const inlineCoverageEnabled = new RawContextKey('testing.inlineCoverageEnabled', false, { type: 'boolean', description: localize('testing.inlineCoverageEnabled', 'Indicates whether inline coverage is shown') }); export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey } = { [TestRunProfileBitset.Run]: hasRunnableTests, diff --git a/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts b/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts index b8053615c6b..47be6dfacba 100644 --- a/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts +++ b/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts @@ -177,10 +177,10 @@ export class TestingContinuousRunService extends Disposable implements ITestingC if (actualProfiles.length) { this.testService.startContinuousRun({ continuous: true, + group: actualProfiles[0].group, targets: actualProfiles.map(p => ({ testIds: [testId ?? p.controllerId], controllerId: p.controllerId, - profileGroup: p.group, profileId: p.profileId })), }, cts.token); diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/nameProjection.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/nameProjection.test.ts index ea3ab5dfe0f..f0709ccc4e5 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/nameProjection.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/nameProjection.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ListProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/listProjection'; @@ -92,4 +92,3 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { ]); }); }); - diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/treeProjection.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/treeProjection.test.ts index cebf9e01a5d..d59d15ef1f8 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/treeProjection.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/treeProjection.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -267,5 +267,52 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { ]); }); -}); + test('fixes #213316 (single root)', async () => { + harness.flush(); + assert.deepStrictEqual(harness.tree.getRendered(), [ + { e: 'a' }, { e: 'b' } + ]); + harness.pushDiff({ + op: TestDiffOpType.Remove, + itemId: new TestId(['ctrlId', 'id-a']).toString(), + }); + harness.flush(); + assert.deepStrictEqual(harness.tree.getRendered(), [ + { e: 'b' } + ]); + }); + + test('fixes #213316 (multi root)', async () => { + harness.pushDiff({ + op: TestDiffOpType.Add, + item: { controllerId: 'ctrl2', expand: TestItemExpandState.Expanded, item: new TestTestItem(new TestId(['ctrlId2']), 'c').toTestItem() }, + }, { + op: TestDiffOpType.Add, + item: { controllerId: 'ctrl2', expand: TestItemExpandState.NotExpandable, item: new TestTestItem(new TestId(['ctrlId2', 'id-c']), 'ca').toTestItem() }, + }); + harness.flush(); + assert.deepStrictEqual(harness.flush(), [ + { e: 'c', children: [{ e: 'ca' }] }, + { e: 'root', children: [{ e: 'a' }, { e: 'b' }] } + ]); + harness.pushDiff({ + op: TestDiffOpType.Remove, + itemId: new TestId(['ctrlId', 'id-a']).toString(), + }); + harness.flush(); + assert.deepStrictEqual(harness.tree.getRendered(), [ + { e: 'c', children: [{ e: 'ca' }] }, + { e: 'root', children: [{ e: 'b' }] } + ]); + + harness.pushDiff({ + op: TestDiffOpType.Remove, + itemId: new TestId(['ctrlId', 'id-b']).toString(), + }); + harness.flush(); + assert.deepStrictEqual(harness.tree.getRendered(), [ + { e: 'ca' } + ]); + }); +}); diff --git a/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts b/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts index ea8bb1e5b6e..9ab4c553649 100644 --- a/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts @@ -8,14 +8,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { SinonSandbox, createSandbox } from 'sinon'; -import { Iterable } from 'vs/base/common/iterator'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { onObservableChange } from 'vs/workbench/contrib/testing/common/observableUtils'; import { ICoverageAccessor, TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; -import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { IFileCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; @@ -133,58 +131,4 @@ suite('TestCoverage', () => { ], ]); }); - - test('adds per-test data to files', async () => { - const { raw1 } = addTests(); - - const raw3: IFileCoverage = { - id: '1', - testId: TestId.fromString('my-test'), - uri: URI.file('/path/to/file'), - statement: { covered: 12, total: 24 }, - branch: { covered: 7, total: 10 }, - declaration: { covered: 2, total: 5 }, - }; - testCoverage.append(raw3, undefined); - - const fileCoverage = testCoverage.getUri(raw1.uri); - assert.strictEqual(fileCoverage?.perTestData?.size, 1); - - const perTestCoverage = Iterable.first(fileCoverage!.perTestData!.values()); - assert.deepStrictEqual(perTestCoverage?.statement, raw3.statement); - assert.deepStrictEqual(perTestCoverage?.branch, raw3.branch); - assert.deepStrictEqual(perTestCoverage?.declaration, raw3.declaration); - - // should be unchanged: - assert.deepEqual(fileCoverage?.statement, { covered: 10, total: 20 }); - const dirCoverage = testCoverage.getComputedForUri(URI.file('/path/to')); - assert.deepEqual(dirCoverage?.statement, { covered: 15, total: 30 }); - }); - - test('works if per-test data is added first', async () => { - const raw3: IFileCoverage = { - id: '1', - testId: TestId.fromString('my-test'), - uri: URI.file('/path/to/file'), - statement: { covered: 12, total: 24 }, - branch: { covered: 7, total: 10 }, - declaration: { covered: 2, total: 5 }, - }; - testCoverage.append(raw3, undefined); - - const fileCoverage = testCoverage.getUri(raw3.uri); - - addTests(); - - assert.strictEqual(fileCoverage?.perTestData?.size, 1); - const perTestCoverage = Iterable.first(fileCoverage!.perTestData!.values()); - assert.deepStrictEqual(perTestCoverage?.statement, raw3.statement); - assert.deepStrictEqual(perTestCoverage?.branch, raw3.branch); - assert.deepStrictEqual(perTestCoverage?.declaration, raw3.declaration); - - // should be the expected values: - assert.deepEqual(fileCoverage?.statement, { covered: 10, total: 20 }); - const dirCoverage = testCoverage.getComputedForUri(URI.file('/path/to')); - assert.deepEqual(dirCoverage?.statement, { covered: 15, total: 30 }); - }); }); diff --git a/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts b/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts index 2be30fb7d2a..192515892c7 100644 --- a/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { InMemoryStorageService } from 'vs/platform/storage/common/storage'; diff --git a/src/vs/workbench/contrib/testing/test/common/testProfileService.test.ts b/src/vs/workbench/contrib/testing/test/common/testProfileService.test.ts index 1d5771ae4bb..c83d3e7d3b9 100644 --- a/src/vs/workbench/contrib/testing/test/common/testProfileService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testProfileService.test.ts @@ -5,7 +5,7 @@ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; diff --git a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts index da47d907d00..4ccd1f6da36 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -32,8 +32,8 @@ suite('Workbench - Test Results Service', () => { let tests: TestTestCollection; const defaultOpts = (testIds: string[]): ResolvedTestRunRequest => ({ + group: TestRunProfileBitset.Run, targets: [{ - profileGroup: TestRunProfileBitset.Run, profileId: 0, controllerId: 'ctrlId', testIds, diff --git a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts index d040484a930..c7c61884c44 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { range } from 'vs/base/common/arrays'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -12,6 +12,7 @@ import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtil import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { ITestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { InMemoryResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage'; +import { TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { testStubs } from 'vs/workbench/contrib/testing/test/common/testStubs'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -23,7 +24,7 @@ suite('Workbench - Test Result Storage', () => { const t = ds.add(new LiveTestResult( '', true, - { targets: [] }, + { targets: [], group: TestRunProfileBitset.Run }, NullTelemetryService, )); diff --git a/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts b/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts index 6dd030f9b53..6e9ce5b745a 100644 --- a/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; diff --git a/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts b/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts index ee279e63089..710fb4da3c4 100644 --- a/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts +++ b/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts @@ -8,7 +8,7 @@ import { IColorRegistry, Extensions, ColorContribution, asCssVariableName } from import { asTextOrError } from 'vs/platform/request/common/request'; import * as pfs from 'vs/base/node/pfs'; import * as path from 'vs/base/common/path'; -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { RequestService } from 'vs/platform/request/node/requestService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index f152f086e9d..6ca11ce3e4e 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -574,6 +574,15 @@ export class TimelinePane extends ViewPane { } if (options === undefined) { + if ( + !reset && + timeline !== undefined && + timeline.items.length > 0 && + !timeline.more + ) { + // If we are not resetting, have item(s), and already know there are no more to fetch, we're done here + return false; + } options = { cursor: reset ? undefined : timeline?.cursor, limit: this.pageSize }; } diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 9f6cc07ba5f..78f429a64bb 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -34,6 +34,7 @@ import { SimpleSettingRenderer } from 'vs/workbench/contrib/markdown/browser/mar import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Schemas } from 'vs/base/common/network'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { marked } from 'vs/base/common/marked/marked'; export class ReleaseNotesManager { private readonly _simpleSettingRenderer: SimpleSettingRenderer; @@ -249,7 +250,10 @@ export class ReleaseNotesManager { private async renderBody(text: string) { const nonce = generateUuid(); - const content = await renderMarkdownDocument(text, this._extensionService, this._languageService, false, undefined, undefined, this._simpleSettingRenderer); + const renderer = new marked.Renderer(); + renderer.html = this._simpleSettingRenderer.getHtmlRenderer(); + + const content = await renderMarkdownDocument(text, this._extensionService, this._languageService, { shouldSanitize: false, renderer }); const colorMap = TokenizationRegistry.getColorMap(); const css = colorMap ? generateTokensCSSForColorMap(colorMap) : ''; const showReleaseNotes = Boolean(this._configurationService.getValue('update.showReleaseNotes')); diff --git a/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts b/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts index baab39f82b6..db1c149501f 100644 --- a/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts +++ b/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css index 0233dfd5309..86a5a8d0119 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css +++ b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css @@ -15,15 +15,26 @@ height: 100%; } -.profiles-editor .contents-container, -.profiles-editor .sidebar-container { +.profiles-editor .monaco-split-view2 > .sash-container, +.profiles-editor .monaco-split-view2.separator-border.horizontal > .monaco-scrollable-element > .split-view-container > .split-view-view:not(:first-child)::before { + top: 55px; +} + +.profiles-editor .contents-container { padding: 0px 20px; height: 100%; } +.profiles-editor .sidebar-container { + padding-left: 20px; + height: 100%; +} + .profiles-editor .sidebar-container .new-profile-button { + padding: 0px 20px 0px 18px; display: flex; align-items: center; + height: 40px; } .profiles-editor .sidebar-container .new-profile-button > .monaco-button-dropdown { @@ -36,20 +47,36 @@ padding: 0 4px; } -.profiles-editor .sidebar-container .profiles-tree { - margin-top: 10px; +.profiles-editor .monaco-list-row .profile-tree-item-actions-container { + display: none; +} + +.profiles-editor .monaco-list-row.focused .profile-tree-item-actions-container, +.profiles-editor .monaco-list-row.selected .profile-tree-item-actions-container, +.profiles-editor .monaco-list-row:hover .profile-tree-item-actions-container { + display: flex; + align-items: center; +} + +.profiles-editor .sidebar-container .profiles-list { + margin-top: 15px; } -.profiles-editor .sidebar-container .profiles-tree .profile-tree-item { +.profiles-editor .sidebar-container .profiles-list .profile-list-item { + padding-left: 20px; display: flex; align-items: center; } -.profiles-editor .sidebar-container .profiles-tree .profile-tree-item > * { +.profiles-editor .sidebar-container .profiles-list .profile-list-item > * { margin-right: 5px; } -.profiles-editor .sidebar-container .profiles-tree .profile-tree-item > .profile-tree-item-description { +.profiles-editor .sidebar-container .profiles-list .profile-list-item > .profile-list-item-label.new-profile { + font-style: italic; +} + +.profiles-editor .sidebar-container .profiles-list .profile-list-item > .profile-list-item-description { margin-left: 2px; display: flex; align-items: center; @@ -57,37 +84,66 @@ opacity: 0.7; } +.profiles-editor .sidebar-container .profiles-list .profile-list-item .profile-tree-item-actions-container { + flex: 1; + justify-content: flex-end; + margin-right: 10px; +} + .profiles-editor .hide { display: none !important; } .profiles-editor .contents-container .profile-header { display: flex; - height: 34px; + height: 40px; align-items: center; } -.profiles-editor .contents-container .profile-header .profile-title { - font-size: x-large; - font-weight: bold; +.profiles-editor .contents-container .profile-header .profile-title-container { + flex: 1; + display: flex; + align-items: center; + font-size: medium; +} + +.profiles-editor .contents-container .profile-title-container .codicon { + cursor: pointer; + font-size: large; + padding: 4px; + margin-right: 8px; + border-radius: 5px; +} + +.profiles-editor .contents-container .profile-title-container .codicon.disabled { + cursor: default; +} + +.profiles-editor .contents-container .profile-title-container .codicon:not(.disabled):hover { + background-color: var(--vscode-toolbar-hoverBackground); + outline: 1px dashed var(--vscode-toolbar-hoverOutline); +} + +.profiles-editor .contents-container .profile-title-container .monaco-inputbox { + margin-right: 10px; flex: 1; } -.profiles-editor .contents-container .profile-header .profile-actions-container { +.profiles-editor .contents-container .profile-header .profile-button-container { display: flex; - height: 28px; + align-items: center; } -.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container { - gap: 4px; +.profiles-editor .contents-container .profile-header .profile-button-container .monaco-button { + margin-left: 4px; } -.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container .codicon { - font-size: 18px; +.profiles-editor .contents-container .profile-header .profile-actions-container { + display: flex; } .profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container { - margin-right: 5px; + margin-right: 6px; min-width: 120px; } @@ -96,26 +152,12 @@ padding-right: 10px; } -.profiles-editor .contents-container .profile-body { - margin-top: 20px; +.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container .action-label { + padding: 6px; } -.profiles-editor .contents-container .profile-name-container { - margin: 0px 0px 20px 15px; - display: flex; - width: 330px; - align-items: center; -} - -.profiles-editor .contents-container .profile-name-container .codicon { - cursor: pointer; - font-size: 20px; - padding: 2px; -} - -.profiles-editor .contents-container .profile-name-container .monaco-inputbox { - flex: 1; - margin-left: 10px; +.profiles-editor .contents-container .profile-body { + margin-top: 20px; } .profiles-editor .contents-container .profile-select-container { @@ -127,19 +169,19 @@ .profiles-editor .contents-container .profile-select-container > .monaco-select-box { cursor: pointer; - line-height: 17px; - padding: 2px 23px 2px 8px; + line-height: 18px; + padding: 0px 23px 0px 8px; border-radius: 2px; } .profiles-editor .contents-container .profile-copy-from-container { display: flex; align-items: center; - margin: 0px 0px 20px 20px; + margin: 0px 0px 15px 36px; } .profiles-editor .contents-container .profile-copy-from-container > .profile-copy-from-label { - margin-right: 10px; + margin-right: 25px; display: inline-flex; align-items: center; } @@ -148,26 +190,83 @@ width: 250px; } +.profiles-editor .contents-container .profile-use-as-default-container { + display: flex; + align-items: center; + margin: 0px 20px 15px 6px; + cursor: pointer; +} + +.profiles-editor .contents-container .profile-use-as-default-container .profile-use-as-default-label { + margin-left: 2px; +} + .profiles-editor .contents-container .profile-contents-container { margin: 0px 0px 10px 20px; - font-size: medium; +} + +.profiles-editor .contents-container .profile-content-tree-header, +.profiles-editor .contents-container .profile-content-tree { + margin-left: 6px; +} + +.profiles-editor .contents-container .profile-content-tree-header { + display: grid; + grid-template-columns: 30px repeat(1, 1fr) 150px 100px; + height: 24px; + align-items: center; + margin-bottom: 2px; + background-color: var(--vscode-keybindingTable-headerBackground); + font-weight: bold; } .profiles-editor .contents-container .profile-tree-item-container { + display: grid; + align-items: center; +} + +.profiles-editor .contents-container .profile-tree-item-container.existing-profile-resource-type-container { + grid-template-columns: repeat(1, 1fr) 150px 100px; +} + +.profiles-editor .contents-container .profile-content-tree-header > .inherit-label, +.profiles-editor .contents-container .profile-tree-item-container > .inherit-container { + justify-content: center; + align-items: center; +} + +.profiles-editor .contents-container .profile-tree-item-container > .inherit-container { + padding-left: 50px; +} + +.profiles-editor .contents-container .profile-content-tree-header > .actions-label { display: flex; + justify-content: center; align-items: center; } -.profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container > .profile-resource-type-label-container { - width: 150px; +.profiles-editor .contents-container .profile-content-tree-header.new-profile { + grid-template-columns: 30px repeat(2, 1fr) 100px; +} + +.profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container { + grid-template-columns: repeat(2, 1fr) 100px; } -.profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container > .profile-select-container { +.profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container .profile-select-container { width: 170px; } +.profiles-editor .contents-container .profile-tree-item-container.profile-resource-child-container { + grid-template-columns: repeat(1, 1fr) 100px; +} + .profiles-editor .contents-container .profile-tree-item-container .profile-resource-type-description { margin-left: 10px; font-size: 0.9em; opacity: 0.7; } + +.profiles-editor .contents-container .profile-tree-item-container .profile-tree-item-actions-container { + justify-content: center; +} diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index f215f1fcbfd..caa0aaa3168 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -30,9 +30,19 @@ import { UserDataProfilesEditor, UserDataProfilesEditorInput, UserDataProfilesEd import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IUserDataProfilesEditor } from 'vs/workbench/contrib/userDataProfile/common/userDataProfile'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import { IProductService } from 'vs/platform/product/common/productService'; type IProfileTemplateQuickPickItem = IQuickPickItem & IProfileTemplateInfo; +export const OpenProfileMenu = new MenuId('OpenProfile'); +const CONFIG_ENABLE_NEW_PROFILES_UI = 'workbench.experimental.enableNewProfilesUI'; +const CONTEXT_ENABLE_NEW_PROFILES_UI = ContextKeyExpr.equals('config.workbench.experimental.enableNewProfilesUI', true); + export class UserDataProfilesWorkbenchContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.userDataProfiles'; @@ -51,6 +61,10 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService, @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IProductService private readonly productService: IProductService, @ILifecycleService private readonly lifecycleService: ILifecycleService, ) { super(); @@ -70,6 +84,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this.hasProfilesContext.set(this.userDataProfilesService.profiles.length > 1); this._register(this.userDataProfilesService.onDidChangeProfiles(e => this.hasProfilesContext.set(this.userDataProfilesService.profiles.length > 1))); + this.registerConfiguration(); this.registerEditor(); this.registerActions(); @@ -80,6 +95,29 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this.reportWorkspaceProfileInfo(); } + private openProfilesEditor(): Promise { + return this.editorGroupsService.activeGroup.openEditor(new UserDataProfilesEditorInput(this.instantiationService)); + } + + private isNewProfilesUIEnabled(): boolean { + return this.configurationService.getValue(CONFIG_ENABLE_NEW_PROFILES_UI) === true; + } + + private registerConfiguration(): void { + Registry.as(Extensions.Configuration) + .registerConfiguration({ + ...workbenchConfigurationNodeBase, + properties: { + [CONFIG_ENABLE_NEW_PROFILES_UI]: { + type: 'boolean', + description: localize('enable new profiles UI', "Enables the new profiles UI."), + default: this.productService.quality !== 'stable', + scope: ConfigurationScope.APPLICATION, + } + } + }); + } + private registerEditor(): void { Registry.as(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( @@ -99,6 +137,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this._register(this.registerManageProfilesAction()); this._register(this.registerSwitchProfileAction()); + this.registerOpenProfileSubMenu(); this.registerProfilesActions(); this._register(this.userDataProfilesService.onDidChangeProfiles(() => this.registerProfilesActions())); @@ -116,6 +155,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements const getProfilesTitle = () => { return localize('profiles', "Profile ({0})", this.userDataProfileService.currentProfile.name); }; + const when = ContextKeyExpr.or(CONTEXT_ENABLE_NEW_PROFILES_UI.negate(), HAS_PROFILES_CONTEXT); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { get title() { return getProfilesTitle(); @@ -123,6 +163,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements submenu: ProfilesMenu, group: '2_configuration', order: 1, + when, }); MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { get title() { @@ -131,60 +172,28 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements submenu: ProfilesMenu, group: '2_configuration', order: 1, - when: PROFILES_ENABLEMENT_CONTEXT, + when, }); } - private registerManageProfilesAction(): IDisposable { - const disposables = new DisposableStore(); - const when = ContextKeyExpr.equals('config.workbench.experimental.enableNewProfilesUI', true); - disposables.add(registerAction2(class ManageProfilesAction extends Action2 { - constructor() { - super({ - id: `workbench.profiles.actions.manageProfiles`, - title: { - ...localize2('manage profiles', "Profiles"), - mnemonicTitle: localize({ key: 'miOpenProfiles', comment: ['&& denotes a mnemonic'] }, "&&Profiles"), - }, - menu: [ - { - id: MenuId.GlobalActivity, - group: '2_configuration', - when, - order: 1 - }, - { - id: MenuId.MenubarPreferencesMenu, - group: '2_configuration', - when, - order: 1 - } - ] - }); - } - run(accessor: ServicesAccessor) { - const editorGroupsService = accessor.get(IEditorGroupsService); - const instantiationService = accessor.get(IInstantiationService); - return editorGroupsService.activeGroup.openEditor(new UserDataProfilesEditorInput(instantiationService)); - } - })); - disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: 'workbench.profiles.actions.manageProfiles', - category: Categories.Preferences, - title: localize2('open profiles', "Open Profiles (UI)"), - precondition: when, - }, - })); - - return disposables; + private registerOpenProfileSubMenu(): void { + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + title: localize('New Profile Window', "New Window with Profile"), + submenu: OpenProfileMenu, + group: '1_new', + order: 4, + when: HAS_PROFILES_CONTEXT, + }); } private readonly profilesDisposable = this._register(new MutableDisposable()); private registerProfilesActions(): void { this.profilesDisposable.value = new DisposableStore(); for (const profile of this.userDataProfilesService.profiles) { - this.profilesDisposable.value.add(this.registerProfileEntryAction(profile)); + if (!profile.isTransient) { + this.profilesDisposable.value.add(this.registerProfileEntryAction(profile)); + this.profilesDisposable.value.add(this.registerNewWindowAction(profile)); + } } } @@ -226,6 +235,42 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements }); } + private registerNewWindowAction(profile: IUserDataProfile): IDisposable { + const disposables = new DisposableStore(); + + const id = `workbench.action.openProfile.${profile.name.toLowerCase().replace('/\s+/', '_')}`; + + disposables.add(registerAction2(class NewWindowAction extends Action2 { + + constructor() { + super({ + id, + title: localize2('openShort', "{0}", profile.name), + menu: { + id: OpenProfileMenu, + when: HAS_PROFILES_CONTEXT + } + }); + } + + override run(accessor: ServicesAccessor): Promise { + const hostService = accessor.get(IHostService); + return hostService.openWindow({ remoteAuthority: null, forceProfile: profile.name }); + } + })); + + disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id, + category: PROFILES_CATEGORY, + title: localize2('open', "Open {0} Profile", profile.name), + precondition: HAS_PROFILES_CONTEXT + }, + })); + + return disposables; + } + private registerSwitchProfileAction(): IDisposable { return registerAction2(class SwitchProfileAction extends Action2 { constructor() { @@ -266,28 +311,76 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this.currentprofileActionsDisposable.value.add(this.registerImportProfileAction()); } + private registerManageProfilesAction(): IDisposable { + const disposables = new DisposableStore(); + disposables.add(registerAction2(class ManageProfilesAction extends Action2 { + constructor() { + super({ + id: `workbench.profiles.actions.manageProfiles`, + title: { + ...localize2('manage profiles', "Profiles"), + mnemonicTitle: localize({ key: 'miOpenProfiles', comment: ['&& denotes a mnemonic'] }, "&&Profiles"), + }, + menu: [ + { + id: MenuId.GlobalActivity, + group: '2_configuration', + order: 1, + when: CONTEXT_ENABLE_NEW_PROFILES_UI, + }, + { + id: MenuId.MenubarPreferencesMenu, + group: '2_configuration', + order: 1, + when: CONTEXT_ENABLE_NEW_PROFILES_UI, + }, + ] + }); + } + run(accessor: ServicesAccessor) { + const editorGroupsService = accessor.get(IEditorGroupsService); + const instantiationService = accessor.get(IInstantiationService); + return editorGroupsService.activeGroup.openEditor(new UserDataProfilesEditorInput(instantiationService)); + } + })); + disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.profiles.actions.manageProfiles', + category: Categories.Preferences, + title: localize2('open profiles', "Open Profiles (UI)"), + precondition: CONTEXT_ENABLE_NEW_PROFILES_UI, + }, + })); + + return disposables; + } + private registerEditCurrentProfileAction(): IDisposable { const that = this; return registerAction2(class RenameCurrentProfileAction extends Action2 { constructor() { - const when = ContextKeyExpr.and(ContextKeyExpr.notEquals(CURRENT_PROFILE_CONTEXT.key, that.userDataProfilesService.defaultProfile.id), IS_CURRENT_PROFILE_TRANSIENT_CONTEXT.toNegated()); + const precondition = ContextKeyExpr.and(ContextKeyExpr.notEquals(CURRENT_PROFILE_CONTEXT.key, that.userDataProfilesService.defaultProfile.id), IS_CURRENT_PROFILE_TRANSIENT_CONTEXT.toNegated()); super({ id: `workbench.profiles.actions.editCurrentProfile`, title: localize2('edit profile', "Edit Profile..."), - precondition: when, + precondition, f1: true, menu: [ { id: ProfilesMenu, group: '2_manage_current', - when, + when: ContextKeyExpr.and(precondition, CONTEXT_ENABLE_NEW_PROFILES_UI.negate()), order: 2 } ] }); } - run() { - return that.userDataProfileImportExportService.editProfile(that.userDataProfileService.currentProfile); + run(accessor: ServicesAccessor) { + if (that.isNewProfilesUIEnabled()) { + return that.openProfilesEditor(); + } else { + return that.userDataProfileImportExportService.editProfile(that.userDataProfileService.currentProfile); + } } }); } @@ -304,9 +397,8 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements { id: ProfilesMenu, group: '2_manage_current', - order: 3 - }, { - id: MenuId.CommandPalette + order: 3, + when: CONTEXT_ENABLE_NEW_PROFILES_UI.negate() } ] }); @@ -334,7 +426,8 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements { id: ProfilesMenu, group: '4_import_export_profiles', - order: 1 + order: 1, + when: CONTEXT_ENABLE_NEW_PROFILES_UI.negate(), }, { id: MenuId.CommandPalette } @@ -343,8 +436,11 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements } async run(accessor: ServicesAccessor) { - const userDataProfileImportExportService = accessor.get(IUserDataProfileImportExportService); - return userDataProfileImportExportService.exportProfile(); + if (that.isNewProfilesUIEnabled()) { + return that.openProfilesEditor(); + } else { + return that.userDataProfileImportExportService.exportProfile2(); + } } })); disposables.add(MenuRegistry.appendMenuItem(MenuId.MenubarShare, { @@ -372,7 +468,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements { id: ProfilesMenu, group: '4_import_export_profiles', - when: PROFILES_ENABLEMENT_CONTEXT, + when: ContextKeyExpr.and(PROFILES_ENABLEMENT_CONTEXT, CONTEXT_ENABLE_NEW_PROFILES_UI.negate()), order: 2 }, { id: MenuId.CommandPalette, @@ -493,7 +589,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements { id: ProfilesMenu, group: '3_manage_profiles', - when: PROFILES_ENABLEMENT_CONTEXT, + when: ContextKeyExpr.and(PROFILES_ENABLEMENT_CONTEXT, CONTEXT_ENABLE_NEW_PROFILES_UI.negate()), order: 1 } ] @@ -501,7 +597,11 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements } async run(accessor: ServicesAccessor) { - return that.userDataProfileImportExportService.createProfile(); + if (that.isNewProfilesUIEnabled()) { + return that.openProfilesEditor(); + } else { + return that.userDataProfileImportExportService.createProfile(); + } } })); } @@ -519,7 +619,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements { id: ProfilesMenu, group: '3_manage_profiles', - when: PROFILES_ENABLEMENT_CONTEXT, + when: ContextKeyExpr.and(PROFILES_ENABLEMENT_CONTEXT, CONTEXT_ENABLE_NEW_PROFILES_UI.negate()), order: 2 } ] diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts index 4c294d03ac2..8ddcc3527b5 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts @@ -20,15 +20,15 @@ import { IEditorOpenContext, IEditorSerializer, IUntypedEditorInput } from 'vs/w import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IUserDataProfilesEditor } from 'vs/workbench/contrib/userDataProfile/common/userDataProfile'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { defaultUserDataProfileIcon, IProfileResourceChildTreeItem, IProfileTemplateInfo, IUserDataProfileManagementService, PROFILE_FILTER } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { defaultUserDataProfileIcon, IProfileTemplateInfo, IUserDataProfileManagementService, PROFILE_FILTER } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { Button, ButtonWithDropdown } from 'vs/base/browser/ui/button/button'; import { defaultButtonStyles, defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; -import { WorkbenchAsyncDataTree, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; -import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IAsyncDataSource, IObjectTreeElement, ITreeNode, ITreeRenderer, ObjectTreeElementCollapseState } from 'vs/base/browser/ui/tree/tree'; +import { WorkbenchAsyncDataTree, WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; @@ -44,23 +44,19 @@ import { IHoverWidget } from 'vs/base/browser/ui/hover/hover'; import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { URI } from 'vs/base/common/uri'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; -import { ExtensionsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/extensionsResource'; -import { isString, isUndefined } from 'vs/base/common/types'; +import { isUndefined } from 'vs/base/common/types'; import { basename } from 'vs/base/common/resources'; import { RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; -import { SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { DEFAULT_LABELS_CONTAINER, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { AbstractUserDataProfileElement, IProfileElement, NewProfileElement, UserDataProfileElement, UserDataProfilesEditorModel } from 'vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel'; +import { AbstractUserDataProfileElement, isProfileResourceChildElement, isProfileResourceTypeElement, IProfileChildElement, IProfileResourceTypeChildElement, IProfileResourceTypeElement, NewProfileElement, UserDataProfileElement, UserDataProfilesEditorModel } from 'vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel'; import { Codicon } from 'vs/base/common/codicons'; import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -export const profilesSashBorder = registerColor('profiles.sashBorder', { dark: PANEL_BORDER, light: PANEL_BORDER, hcDark: PANEL_BORDER, hcLight: PANEL_BORDER }, localize('profilesSashBorder', "The color of the Profiles editor splitview sash border.")); +export const profilesSashBorder = registerColor('profiles.sashBorder', PANEL_BORDER, localize('profilesSashBorder', "The color of the Profiles editor splitview sash border.")); export class UserDataProfilesEditor extends EditorPane implements IUserDataProfilesEditor { @@ -68,7 +64,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi private container: HTMLElement | undefined; private splitView: SplitView | undefined; - private profilesTree: WorkbenchObjectTree | undefined; + private profilesList: WorkbenchList | undefined; private profileWidget: ProfileWidget | undefined; private model: UserDataProfilesEditorModel | undefined; @@ -81,7 +77,6 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi @IStorageService storageService: IStorageService, @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IDialogService private readonly dialogService: IDialogService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -116,20 +111,21 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi this.splitView.addView({ onDidChange: Event.None, element: sidebarView, - minimumSize: 175, + minimumSize: 200, maximumSize: 350, layout: (width, _, height) => { sidebarView.style.width = `${width}px`; - if (height && this.profilesTree) { - this.profilesTree.getHTMLElement().style.height = `${height - 38}px`; - this.profilesTree.layout(height - 38, width); + if (height && this.profilesList) { + const listHeight = height - 40 /* new profile button */ - 15 /* marginTop */; + this.profilesList.getHTMLElement().style.height = `${listHeight}px`; + this.profilesList.layout(listHeight, width); } } }, 300, undefined, true); this.splitView.addView({ onDidChange: Event.None, element: contentsView, - minimumSize: 500, + minimumSize: 550, maximumSize: Number.POSITIVE_INFINITY, layout: (width, _, height) => { contentsView.style.width = `${width}px`; @@ -139,10 +135,8 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi } }, Sizing.Distribute, undefined, true); - const borderColor = this.theme.getColor(profilesSashBorder)!; - this.splitView.style({ separatorBorder: borderColor }); - this.registerListeners(); + this.updateStyles(); this.userDataProfileManagementService.getBuiltinProfileTemplates().then(templates => { this.templates = templates; @@ -150,15 +144,20 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi }); } + override updateStyles(): void { + const borderColor = this.theme.getColor(profilesSashBorder)!; + this.splitView?.style({ separatorBorder: borderColor }); + } + private renderSidebar(parent: HTMLElement): void { // render New Profile Button this.renderNewProfileButton(append(parent, $('.new-profile-button'))); - // render profiles and templates tree - const renderer = this.instantiationService.createInstance(ProfileTreeElementRenderer); - const delegate = new ProfileTreeElementDelegate(); - this.profilesTree = this._register(this.instantiationService.createInstance(WorkbenchObjectTree, 'ProfilesTree', - append(parent, $('.profiles-tree')), + // render profiles list + const renderer = this.instantiationService.createInstance(ProfileElementRenderer); + const delegate = new ProfileElementDelegate(); + this.profilesList = this._register(this.instantiationService.createInstance(WorkbenchList, 'ProfilesList', + append(parent, $('.profiles-list')), delegate, [renderer], { @@ -166,15 +165,14 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi setRowLineHeight: false, horizontalScrolling: false, accessibilityProvider: { - getAriaLabel(extensionFeature: IProfileElement | null): string { - return extensionFeature?.name ?? ''; + getAriaLabel(profileElement: AbstractUserDataProfileElement | null): string { + return profileElement?.name ?? ''; }, getWidgetAriaLabel(): string { return localize('profiles', "Profiles"); } }, openOnSingleClick: true, - enableStickyScroll: false, identityProvider: { getId(e) { if (e instanceof UserDataProfileElement) { @@ -192,10 +190,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi getActions: () => { const actions: IAction[] = []; if (this.templates.length) { - actions.push(new SubmenuAction('from.template', localize('from template', "From Template"), - this.templates.map(template => new Action(`template:${template.url}`, template.name, undefined, true, async () => { - this.createNewProfile(URI.parse(template.url)); - })))); + actions.push(new SubmenuAction('from.template', localize('from template', "From Template"), this.getCreateFromTemplateActions())); actions.push(new Separator()); } actions.push(new Action('importProfile', localize('importProfile', "Import Profile..."), undefined, true, () => this.importProfile())); @@ -207,29 +202,58 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi supportIcons: true, ...defaultButtonStyles })); - button.label = `$(add) ${localize('newProfile', "New Profile")}`; + button.label = localize('newProfile', "New Profile"); this._register(button.onDidClick(e => this.createNewProfile())); } + private getCreateFromTemplateActions(): IAction[] { + return this.templates.map(template => new Action(`template:${template.url}`, template.name, undefined, true, async () => { + this.createNewProfile(URI.parse(template.url)); + })); + } + private registerListeners(): void { - if (this.profilesTree) { - this._register(this.profilesTree.onDidChangeSelection(e => { + if (this.profilesList) { + this._register(this.profilesList.onDidChangeSelection(e => { const [element] = e.elements; if (element instanceof AbstractUserDataProfileElement) { this.profileWidget?.render(element); } })); - - this._register(this.profilesTree.onContextMenu(e => { + this._register(this.profilesList.onContextMenu(e => { + const actions: IAction[] = []; + if (!e.element) { + actions.push(...this.getTreeContextMenuActions()); + } if (e.element instanceof AbstractUserDataProfileElement) { + actions.push(...e.element.actions[1]); + } + if (actions.length) { this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, - getActions: () => e.element instanceof AbstractUserDataProfileElement ? e.element.contextMenuActions.slice(0) : [], + getActions: () => actions, getActionsContext: () => e.element }); } })); + this._register(this.profilesList.onMouseDblClick(e => { + if (!e.element) { + this.createNewProfile(); + } + })); + } + } + + private getTreeContextMenuActions(): IAction[] { + const actions: IAction[] = []; + actions.push(new Action('newProfile', localize('newProfile', "New Profile"), undefined, true, () => this.createNewProfile())); + const templateActions = this.getCreateFromTemplateActions(); + if (templateActions.length) { + actions.push(new SubmenuAction('from.template', localize('new from template', "New Profile From Template"), templateActions)); } + actions.push(new Separator()); + actions.push(new Action('importProfile', localize('importProfile', "Import Profile..."), undefined, true, () => this.importProfile())); + return actions; } private async importProfile(): Promise { @@ -268,19 +292,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi } private async createNewProfile(copyFrom?: URI | IUserDataProfile): Promise { - if (this.model?.profiles.some(p => p instanceof NewProfileElement)) { - const result = await this.dialogService.confirm({ - type: 'info', - message: localize('new profile exists', "A new profile is already being created. Do you want to discard it and create a new one?"), - primaryButton: localize('discard', "Discard & Create"), - cancelButton: localize('cancel', "Cancel") - }); - if (!result.confirmed) { - return; - } - this.model.revert(); - } - this.model?.createNewProfile(copyFrom); + await this.model?.createNewProfile(copyFrom); } private async getProfileUriFromFileSystem(): Promise { @@ -300,91 +312,100 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi override async setInput(input: UserDataProfilesEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); this.model = await input.resolve(); - this.updateProfilesTree(); + this.updateProfilesList(); this._register(this.model.onDidChange((element) => { - this.updateProfilesTree(element); + this.updateProfilesList(element); })); } override focus(): void { super.focus(); - this.profilesTree?.domFocus(); + this.profilesList?.domFocus(); } - private updateProfilesTree(elementToSelect?: IProfileElement): void { + private updateProfilesList(elementToSelect?: AbstractUserDataProfileElement): void { if (!this.model) { return; } - const profileElements: IObjectTreeElement[] = this.model.profiles.map(element => ({ element })); - const currentSelection = this.profilesTree?.getSelection()?.[0]; - this.profilesTree?.setChildren(null, [ - { - element: { name: localize('profiles', "Profiles") }, - children: profileElements, - collapsible: false, - collapsed: ObjectTreeElementCollapseState.Expanded - } - ]); + const currentSelectionIndex = this.profilesList?.getSelection()?.[0]; + const currentSelection = currentSelectionIndex !== undefined ? this.profilesList?.element(currentSelectionIndex) : undefined; + this.profilesList?.splice(0, this.profilesList.length, this.model.profiles); + if (elementToSelect) { - this.profilesTree?.setSelection([elementToSelect]); + this.profilesList?.setSelection([this.model.profiles.indexOf(elementToSelect)]); } else if (currentSelection) { - if (currentSelection instanceof AbstractUserDataProfileElement) { - if (!this.model.profiles.includes(currentSelection)) { - const elementToSelect = this.model.profiles.find(profile => profile.name === currentSelection.name) ?? this.model.profiles[0]; - if (elementToSelect) { - this.profilesTree?.setSelection([elementToSelect]); - } + if (!this.model.profiles.includes(currentSelection)) { + const elementToSelect = this.model.profiles.find(profile => profile.name === currentSelection.name) ?? this.model.profiles[0]; + if (elementToSelect) { + this.profilesList?.setSelection([this.model.profiles.indexOf(elementToSelect)]); } } } else { const elementToSelect = this.model.profiles.find(profile => profile.active) ?? this.model.profiles[0]; if (elementToSelect) { - this.profilesTree?.setSelection([elementToSelect]); + this.profilesList?.setSelection([this.model.profiles.indexOf(elementToSelect)]); } } } } -interface IProfileTreeElementTemplateData { +interface IProfileElementTemplateData { readonly icon: HTMLElement; readonly label: HTMLElement; readonly description: HTMLElement; + readonly actionBar: WorkbenchToolBar; readonly disposables: DisposableStore; + readonly elementDisposables: DisposableStore; } -class ProfileTreeElementDelegate implements IListVirtualDelegate { - getHeight(element: IProfileElement) { - return 30; +class ProfileElementDelegate implements IListVirtualDelegate { + getHeight(element: AbstractUserDataProfileElement) { + return 22; } - getTemplateId() { return 'profileTreeElement'; } + getTemplateId() { return 'profileListElement'; } } -class ProfileTreeElementRenderer implements ITreeRenderer { +class ProfileElementRenderer implements IListRenderer { - readonly templateId = 'profileTreeElement'; + readonly templateId = 'profileListElement'; - renderTemplate(container: HTMLElement): IProfileTreeElementTemplateData { - container.classList.add('profile-tree-item'); - const icon = append(container, $('.profile-tree-item-icon')); - const label = append(container, $('.profile-tree-item-label')); - const description = append(container, $('.profile-tree-item-description')); + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { } + + renderTemplate(container: HTMLElement): IProfileElementTemplateData { + + const disposables = new DisposableStore(); + const elementDisposables = new DisposableStore(); + + container.classList.add('profile-list-item'); + const icon = append(container, $('.profile-list-item-icon')); + const label = append(container, $('.profile-list-item-label')); + const description = append(container, $('.profile-list-item-description')); append(description, $(`span${ThemeIcon.asCSSSelector(Codicon.check)}`)); - append(description, $('span', undefined, localize('activeProfile', "Active"))); - return { label, icon, description, disposables: new DisposableStore() }; + append(description, $('span', undefined, localize('activeProfile', "In use"))); + + const actionsContainer = append(container, $('.profile-tree-item-actions-container')); + const actionBar = disposables.add(this.instantiationService.createInstance(WorkbenchToolBar, + actionsContainer, + { + hoverDelegate: disposables.add(createInstantHoverDelegate()), + highlightToggledItems: true + } + )); + + return { label, icon, description, actionBar, disposables, elementDisposables }; } - renderElement({ element }: ITreeNode, index: number, templateData: IProfileTreeElementTemplateData, height: number | undefined): void { - templateData.disposables.clear(); + renderElement(element: AbstractUserDataProfileElement, index: number, templateData: IProfileElementTemplateData, height: number | undefined) { + templateData.elementDisposables.clear(); templateData.label.textContent = element.name; - if (element.icon) { - templateData.icon.className = ThemeIcon.asClassName(ThemeIcon.fromId(element.icon)); - } else { - templateData.icon.className = 'hide'; - } + templateData.label.classList.toggle('new-profile', element instanceof NewProfileElement); + templateData.icon.className = ThemeIcon.asClassName(element.icon ? ThemeIcon.fromId(element.icon) : DEFAULT_ICON); templateData.description.classList.toggle('hide', !element.active); if (element.onDidChange) { - templateData.disposables.add(element.onDidChange(e => { + templateData.elementDisposables.add(element.onDidChange(e => { if (e.name) { templateData.label.textContent = element.name; } @@ -400,10 +421,16 @@ class ProfileTreeElementRenderer implements ITreeRenderer; private _templates: IProfileTemplateInfo[] = []; @@ -435,32 +465,18 @@ class ProfileWidget extends Disposable { @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IContextViewService private readonly contextViewService: IContextViewService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, - @ICommandService private readonly commandService: ICommandService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); const header = append(parent, $('.profile-header')); - const title = append(header, $('.profile-title')); - append(title, $('span', undefined, localize('profile', "Profile: "))); - this.profileTitle = append(title, $('span')); - const actionsContainer = append(header, $('.profile-actions-container')); - this.buttonContainer = append(actionsContainer, $('.profile-button-container')); - this.toolbar = this._register(instantiationService.createInstance(WorkbenchToolBar, - actionsContainer, - { - hoverDelegate: this._register(createInstantHoverDelegate()), - } - )); - - const body = append(parent, $('.profile-body')); - - this.nameContainer = append(body, $('.profile-name-container')); - this.iconElement = append(this.nameContainer, $(`${ThemeIcon.asCSSSelector(DEFAULT_ICON)}`, { 'tabindex': '0', 'role': 'button', 'aria-label': localize('icon', "Profile Icon") })); + const title = append(header, $('.profile-title-container')); + this.iconElement = append(title, $(`${ThemeIcon.asCSSSelector(DEFAULT_ICON)}`, { 'tabindex': '0', 'role': 'button', 'aria-label': localize('icon', "Profile Icon") })); this.renderIconSelectBox(this.iconElement); + this.profileTitle = append(title, $('')); this.nameInput = this._register(new InputBox( - this.nameContainer, + title, undefined, { inputBoxStyles: defaultInputBoxStyles, @@ -474,7 +490,11 @@ class ProfileWidget extends Disposable { type: MessageType.ERROR }; } - const initialName = this._profileElement.value?.element instanceof UserDataProfileElement ? this._profileElement.value.element.profile.name : undefined; + if (this._profileElement.value?.element.disabled) { + return null; + } + const initialName = this._profileElement.value?.element.getInitialName(); + value = value.trim(); if (initialName !== value && this.userDataProfilesService.profiles.some(p => p.name === value)) { return { content: localize('profileExists', "Profile with name {0} already exists.", value), @@ -498,8 +518,20 @@ class ProfileWidget extends Disposable { } })); + const actionsContainer = append(header, $('.profile-actions-container')); + this.buttonContainer = append(actionsContainer, $('.profile-button-container')); + this.toolbar = this._register(instantiationService.createInstance(WorkbenchToolBar, + actionsContainer, + { + hoverDelegate: this._register(createInstantHoverDelegate()), + highlightToggledItems: true + } + )); + + const body = append(parent, $('.profile-body')); + this.copyFromContainer = append(body, $('.profile-copy-from-container')); - append(this.copyFromContainer, $('.profile-copy-from-label', undefined, localize('create from', "Copy from:"))); + append(this.copyFromContainer, $('.profile-copy-from-label', undefined, localize('create from', "Copy from"))); this.copyFromSelectBox = this._register(this.instantiationService.createInstance(SelectBox, [], 0, @@ -512,9 +544,30 @@ class ProfileWidget extends Disposable { )); this.copyFromSelectBox.render(append(this.copyFromContainer, $('.profile-select-container'))); - const contentsContainer = append(body, $('.profile-contents-container')); - append(contentsContainer, $('.profile-contents-label', undefined, localize('contents', "Contents"))); + this.useAsDefaultProfileContainer = append(body, $('.profile-use-as-default-container')); + const useAsDefaultProfileTitle = localize('enable for new windows', "Use this profile as default for new windows"); + this.useAsDefaultProfileCheckbox = this._register(new Checkbox(useAsDefaultProfileTitle, false, defaultCheckboxStyles)); + append(this.useAsDefaultProfileContainer, this.useAsDefaultProfileCheckbox.domNode); + const useAsDefaultProfileLabel = append(this.useAsDefaultProfileContainer, $('.profile-use-as-default-label', undefined, useAsDefaultProfileTitle)); + this._register(this.useAsDefaultProfileCheckbox.onChange(() => { + if (this._profileElement.value?.element instanceof UserDataProfileElement) { + this._profileElement.value.element.toggleNewWindowProfile(); + } + })); + this._register(addDisposableListener(useAsDefaultProfileLabel, EventType.CLICK, () => { + if (this._profileElement.value?.element instanceof UserDataProfileElement) { + this._profileElement.value.element.toggleNewWindowProfile(); + } + })); + this.contentsTreeHeader = append(body, $('.profile-content-tree-header')); + this.inheritLabelElement = $('.inherit-label', undefined, localize('default profile', "Use Default Profile")); + append(this.contentsTreeHeader, + $(''), + $(''), + this.inheritLabelElement, + $('.actions-label', undefined, localize('actions', "Actions")), + ); const delegate = new ProfileResourceTreeElementDelegate(); this.resourcesTree = this._register(this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'ProfileEditor-ResourcesTree', @@ -531,11 +584,11 @@ class ProfileWidget extends Disposable { horizontalScrolling: false, accessibilityProvider: { getAriaLabel(element: ProfileResourceTreeElement | null): string { - if (isString(element?.element)) { - return element.element; + if ((element?.element).resourceType) { + return (element?.element).resourceType; } - if (element?.element) { - return element.element.label?.label ?? ''; + if ((element?.element).label) { + return (element?.element).label; } return ''; }, @@ -545,10 +598,7 @@ class ProfileWidget extends Disposable { }, identityProvider: { getId(element) { - if (isString(element?.element)) { - return element.element; - } - if (element?.element) { + if (element?.element.handle) { return element.element.handle; } return ''; @@ -556,8 +606,8 @@ class ProfileWidget extends Disposable { }, expandOnlyOnTwistieClick: true, renderIndentGuides: RenderIndentGuides.None, - openOnSingleClick: true, enableStickyScroll: false, + openOnSingleClick: false, })); this._register(this.resourcesTree.onDidOpen(async (e) => { if (!e.browserEvent) { @@ -566,12 +616,8 @@ class ProfileWidget extends Disposable { if (e.browserEvent.target && (e.browserEvent.target as HTMLElement).classList.contains(Checkbox.CLASS_NAME)) { return; } - if (e.element && !isString(e.element.element)) { - if (e.element.element.resourceUri) { - await this.commandService.executeCommand(API_OPEN_EDITOR_COMMAND_ID, e.element.element.resourceUri, [SIDE_GROUP], undefined, e); - } else if (e.element.element.parent instanceof ExtensionsResourceTreeItem) { - await this.commandService.executeCommand('extension.open', e.element.element.handle, undefined, true, undefined, true); - } + if (e.element?.element.action) { + await e.element.element.action.run(); } })); } @@ -583,6 +629,9 @@ class ProfileWidget extends Disposable { if (this._profileElement.value?.element instanceof UserDataProfileElement && this._profileElement.value.element.profile.isDefault) { return; } + if (this._profileElement.value?.element.disabled) { + return; + } iconSelectBox.clearInput(); hoverWidget = this.hoverService.showHover({ content: iconSelectBox.domNode, @@ -632,19 +681,7 @@ class ProfileWidget extends Disposable { } private renderSelectBox(): void { - const separator = { text: '\u2500\u2500\u2500\u2500\u2500\u2500', isDisabled: true }; - this.copyFromOptions.push({ text: localize('empty profile', "None") }); - if (this._templates.length) { - this.copyFromOptions.push({ ...separator, decoratorRight: localize('from templates', "Profile Templates") }); - for (const template of this._templates) { - this.copyFromOptions.push({ text: template.name, id: template.url, source: URI.parse(template.url) }); - } - } - this.copyFromOptions.push({ ...separator, decoratorRight: localize('from existing profiles', "Existing Profiles") }); - for (const profile of this.userDataProfilesService.profiles) { - this.copyFromOptions.push({ text: profile.name, id: profile.id, source: profile }); - } - this.copyFromSelectBox.setOptions(this.copyFromOptions); + this.copyFromSelectBox.setOptions(this.getCopyFromOptions()); this._register(this.copyFromSelectBox.onDidSelect(option => { if (this._profileElement.value?.element instanceof NewProfileElement) { this._profileElement.value.element.copyFrom = this.copyFromOptions[option.index].source; @@ -657,6 +694,8 @@ class ProfileWidget extends Disposable { } render(profileElement: AbstractUserDataProfileElement): void { + this.resourcesTree.setInput(profileElement); + const disposables = new DisposableStore(); this._profileElement.value = { element: profileElement, dispose: () => disposables.dispose() }; @@ -664,11 +703,13 @@ class ProfileWidget extends Disposable { disposables.add(profileElement.onDidChange(e => this.renderProfileElement(profileElement))); const profile = profileElement instanceof UserDataProfileElement ? profileElement.profile : undefined; - this.nameInput.setEnabled(!profile?.isDefault); + this.profileTitle.classList.toggle('hide', !profile?.isDefault); + this.nameInput.element.classList.toggle('hide', !!profile?.isDefault); + this.iconElement.classList.toggle('disabled', !!profile?.isDefault); + this.iconElement.setAttribute('tabindex', profile?.isDefault ? '' : '0'); - this.resourcesTree.setInput(profileElement); disposables.add(profileElement.onDidChange(e => { - if (e.flags || e.copyFrom) { + if (e.flags || e.copyFrom || e.copyFlags || e.disabled) { const viewState = this.resourcesTree.getViewState(); this.resourcesTree.setInput(profileElement, { ...viewState, @@ -677,34 +718,57 @@ class ProfileWidget extends Disposable { } })); - if (profileElement.primaryAction) { + const [primaryTitleButtons, secondatyTitleButtons] = profileElement.titleButtons; + if (primaryTitleButtons?.length || secondatyTitleButtons?.length) { this.buttonContainer.classList.remove('hide'); - const button = disposables.add(new Button(this.buttonContainer, { - supportIcons: true, - ...defaultButtonStyles - })); - button.label = profileElement.primaryAction.label; - button.enabled = profileElement.primaryAction.enabled; - disposables.add(button.onDidClick(() => this.editorProgressService.showWhile(profileElement.primaryAction!.run()))); - disposables.add(profileElement.primaryAction.onDidChange((e) => { - if (!isUndefined(e.enabled)) { - button.enabled = profileElement.primaryAction!.enabled; + + if (secondatyTitleButtons?.length) { + for (const action of secondatyTitleButtons) { + const button = disposables.add(new Button(this.buttonContainer, { + ...defaultButtonStyles, + secondary: true + })); + button.label = action.label; + button.enabled = action.enabled; + disposables.add(button.onDidClick(() => this.editorProgressService.showWhile(action.run()))); + disposables.add(action.onDidChange((e) => { + if (!isUndefined(e.enabled)) { + button.enabled = action.enabled; + } + })); } - })); - disposables.add(profileElement.onDidChange(e => { - if (e.message) { - button.setTitle(profileElement.message ?? profileElement.primaryAction!.label); - button.element.classList.toggle('error', !!profileElement.message); + } + + if (primaryTitleButtons?.length) { + for (const action of primaryTitleButtons) { + const button = disposables.add(new Button(this.buttonContainer, { + ...defaultButtonStyles + })); + button.label = action.label; + button.enabled = action.enabled; + disposables.add(button.onDidClick(() => this.editorProgressService.showWhile(action.run()))); + disposables.add(action.onDidChange((e) => { + if (!isUndefined(e.enabled)) { + button.enabled = action.enabled; + } + })); + disposables.add(profileElement.onDidChange(e => { + if (e.message) { + button.setTitle(profileElement.message ?? action.label); + button.element.classList.toggle('error', !!profileElement.message); + } + })); } - })); + } + } else { this.buttonContainer.classList.add('hide'); } this.toolbar.setActions(profileElement.titleActions[0].slice(0), profileElement.titleActions[1].slice(0)); - this.nameInput.focus(); if (profileElement instanceof NewProfileElement) { + this.nameInput.focus(); this.nameInput.select(); } } @@ -712,40 +776,71 @@ class ProfileWidget extends Disposable { private renderProfileElement(profileElement: AbstractUserDataProfileElement): void { this.profileTitle.textContent = profileElement.name; this.nameInput.value = profileElement.name; + this.nameInput.validate(); + if (profileElement.disabled) { + this.nameInput.disable(); + } else { + this.nameInput.enable(); + } if (profileElement.icon) { this.iconElement.className = ThemeIcon.asClassName(ThemeIcon.fromId(profileElement.icon)); } else { this.iconElement.className = ThemeIcon.asClassName(ThemeIcon.fromId(DEFAULT_ICON.id)); } if (profileElement instanceof NewProfileElement) { + this.contentsTreeHeader.classList.add('new-profile'); + this.inheritLabelElement.textContent = localize('options', "Options"); + this.useAsDefaultProfileContainer.classList.add('hide'); this.copyFromContainer.classList.remove('hide'); + this.copyFromOptions = this.getCopyFromOptions(); const id = profileElement.copyFrom instanceof URI ? profileElement.copyFrom.toString() : profileElement.copyFrom?.id; const index = id ? this.copyFromOptions.findIndex(option => option.id === id) : 0; if (index !== -1) { this.copyFromSelectBox.setOptions(this.copyFromOptions); - this.copyFromSelectBox.setEnabled(true); + this.copyFromSelectBox.setEnabled(!profileElement.previewProfile && !profileElement.disabled); this.copyFromSelectBox.select(index); } else { this.copyFromSelectBox.setOptions([{ text: basename(profileElement.copyFrom as URI) }]); this.copyFromSelectBox.setEnabled(false); } - } else { + } else if (profileElement instanceof UserDataProfileElement) { + this.contentsTreeHeader.classList.remove('new-profile'); + this.inheritLabelElement.textContent = profileElement.profile.isDefault ? '' : localize('default profile', "Use Default Profile"); + this.useAsDefaultProfileContainer.classList.remove('hide'); + this.useAsDefaultProfileCheckbox.checked = profileElement.isNewWindowProfile; this.copyFromContainer.classList.add('hide'); } } + + private getCopyFromOptions(): (ISelectOptionItem & { id?: string; source?: IUserDataProfile | URI })[] { + const separator = { text: '\u2500\u2500\u2500\u2500\u2500\u2500', isDisabled: true }; + const copyFromOptions: (ISelectOptionItem & { id?: string; source?: IUserDataProfile | URI })[] = []; + copyFromOptions.push({ text: localize('empty profile', "None") }); + if (this._templates.length) { + copyFromOptions.push({ ...separator, decoratorRight: localize('from templates', "Profile Templates") }); + for (const template of this._templates) { + copyFromOptions.push({ text: template.name, id: template.url, source: URI.parse(template.url) }); + } + } + copyFromOptions.push({ ...separator, decoratorRight: localize('from existing profiles', "Existing Profiles") }); + for (const profile of this.userDataProfilesService.profiles) { + copyFromOptions.push({ text: profile.name, id: profile.id, source: profile }); + } + return copyFromOptions; + } } interface ProfileResourceTreeElement { - element: ProfileResourceType | IProfileResourceChildTreeItem; + element: IProfileChildElement; root: AbstractUserDataProfileElement; } class ProfileResourceTreeElementDelegate implements IListVirtualDelegate { getTemplateId(element: ProfileResourceTreeElement) { - if (!isString(element.element)) { + if (!(element.element).resourceType) { return ProfileResourceChildTreeItemRenderer.TEMPLATE_ID; } if (element.root instanceof NewProfileElement) { @@ -754,7 +849,7 @@ class ProfileResourceTreeElementDelegate implements IListVirtualDelegateelement.element).resourceType) { + if ((element.element).resourceType !== ProfileResourceType.Extensions && (element.element).resourceType !== ProfileResourceType.Snippets) { return false; } if (element.root instanceof NewProfileElement) { - return element.root.copyFrom !== undefined; + const resourceType = (element.element).resourceType; + if (element.root.getFlag(resourceType)) { + return true; + } + if (!element.root.hasResource(resourceType)) { + return false; + } + if (element.root.copyFrom === undefined) { + return false; + } + if (!element.root.getCopyFlag(resourceType)) { + return false; + } } return true; } @@ -782,20 +889,14 @@ class ProfileResourceTreeDataSource implements IAsyncDataSource { if (element instanceof AbstractUserDataProfileElement) { - const resourceTypes = [ - ProfileResourceType.Settings, - ProfileResourceType.Keybindings, - ProfileResourceType.Snippets, - ProfileResourceType.Tasks, - ProfileResourceType.Extensions - ]; - return resourceTypes.map(resourceType => ({ element: resourceType, root: element })); + const children = await element.getChildren(); + return children.map(e => ({ element: e, root: element })); } - if (isString(element.element)) { - const progressRunner = this.editorProgressService.show(true); + if ((element.element).resourceType) { + const progressRunner = this.editorProgressService.show(true, 500); try { - const extensions = await element.root.getChildren(element.element); - return extensions.map(extension => ({ element: extension, root: element.root })); + const extensions = await element.root.getChildren((element.element).resourceType); + return extensions.map(e => ({ element: e, root: element.root })); } finally { progressRunner.done(); } @@ -807,12 +908,13 @@ class ProfileResourceTreeDataSource implements IAsyncDataSource, index: number, templateData: IExistingProfileResourceTemplateData, height: number | undefined): void { @@ -876,27 +995,20 @@ class ExistingProfileResourceTreeRenderer extends AbstractProfileResourceTreeRen if (!(root instanceof UserDataProfileElement)) { throw new Error('ExistingProfileResourceTreeRenderer can only render existing profile element'); } - if (!isString(element)) { - throw new Error('ExistingProfileResourceTreeRenderer can only render profile resource types'); + if (!isProfileResourceTypeElement(element)) { + throw new Error('Invalid profile resource element'); } - templateData.label.textContent = this.getResourceTypeTitle(element); + templateData.label.textContent = this.getResourceTypeTitle(element.resourceType); if (root instanceof UserDataProfileElement && root.profile.isDefault) { - templateData.checkbox.checked = true; - templateData.checkbox.disable(); - templateData.description.classList.add('hide'); + templateData.checkbox.domNode.classList.add('hide'); } else { - templateData.checkbox.enable(); - const checked = !root.getFlag(element); - templateData.checkbox.checked = checked; - templateData.description.classList.toggle('hide', checked); - templateData.elementDisposables.add(templateData.checkbox.onChange(() => root.setFlag(element, !templateData.checkbox.checked))); - templateData.elementDisposables.add(root.onDidChange(e => { - if (e.flags) { - templateData.description.classList.toggle('hide', !root.getFlag(element)); - } - })); + templateData.checkbox.domNode.classList.remove('hide'); + templateData.checkbox.checked = root.getFlag(element.resourceType); + templateData.elementDisposables.add(templateData.checkbox.onChange(() => root.setFlag(element.resourceType, templateData.checkbox.checked))); } + + templateData.actionBar.setActions(element.action ? [element.action] : []); } } @@ -920,11 +1032,7 @@ class NewProfileResourceTreeRenderer extends AbstractProfileResourceTreeRenderer const labelContainer = append(container, $('.profile-resource-type-label-container')); const label = append(labelContainer, $('span.profile-resource-type-label')); const selectBox = this._register(this.instantiationService.createInstance(SelectBox, - [ - { text: localize('empty', "Empty") }, - { text: localize('copy', "Copy") }, - { text: localize('default', "Use Default Profile") } - ], + [], 0, this.contextViewService, defaultSelectBoxStyles, @@ -935,7 +1043,16 @@ class NewProfileResourceTreeRenderer extends AbstractProfileResourceTreeRenderer const selectContainer = append(container, $('.profile-select-container')); selectBox.render(selectContainer); - return { label, selectContainer, selectBox, disposables, elementDisposables: disposables.add(new DisposableStore()) }; + const actionsContainer = append(container, $('.profile-tree-item-actions-container')); + const actionBar = disposables.add(this.instantiationService.createInstance(WorkbenchToolBar, + actionsContainer, + { + hoverDelegate: disposables.add(createInstantHoverDelegate()), + highlightToggledItems: true + } + )); + + return { label, selectContainer, selectBox, actionBar, disposables, elementDisposables: disposables.add(new DisposableStore()) }; } renderElement({ element: profileResourceTreeElement }: ITreeNode, index: number, templateData: INewProfileResourceTemplateData, height: number | undefined): void { @@ -944,15 +1061,34 @@ class NewProfileResourceTreeRenderer extends AbstractProfileResourceTreeRenderer if (!(root instanceof NewProfileElement)) { throw new Error('NewProfileResourceTreeRenderer can only render new profile element'); } - if (!isString(element)) { - throw new Error('NewProfileResourceTreeRenderer can only profile resoyrce types'); + if (!isProfileResourceTypeElement(element)) { + throw new Error('Invalid profile resource element'); } - templateData.label.textContent = this.getResourceTypeTitle(element); - templateData.selectBox.select(root.getCopyFlag(element) ? 1 : root.getFlag(element) ? 2 : 0); - templateData.elementDisposables.add(templateData.selectBox.onDidSelect(option => { - root.setFlag(element, option.index === 2); - root.setCopyFlag(element, option.index === 1); - })); + templateData.label.textContent = this.getResourceTypeTitle(element.resourceType); + if (root.copyFrom && root.hasResource(element.resourceType)) { + const copyFromName = root.getCopyFromName(); + templateData.selectBox.setOptions([ + { text: localize('empty', "Empty") }, + { text: copyFromName ? localize('copy from', "Copy ({0})", copyFromName) : localize('copy', "Copy") }, + { text: localize('default', "Use Default Profile") } + ]); + templateData.selectBox.select(root.getCopyFlag(element.resourceType) ? 1 : root.getFlag(element.resourceType) ? 2 : 0); + templateData.elementDisposables.add(templateData.selectBox.onDidSelect(option => { + root.setFlag(element.resourceType, option.index === 2); + root.setCopyFlag(element.resourceType, option.index === 1); + })); + } else { + templateData.selectBox.setOptions([ + { text: localize('empty', "Empty") }, + { text: localize('default', "Use Default Profile") } + ]); + templateData.selectBox.select(root.getFlag(element.resourceType) ? 1 : 0); + templateData.elementDisposables.add(templateData.selectBox.onDidSelect(option => { + root.setFlag(element.resourceType, option.index === 1); + })); + } + templateData.selectBox.setEnabled(!root.disabled); + templateData.actionBar.setActions(element.action ? [element.action] : []); } } @@ -965,7 +1101,7 @@ class ProfileResourceChildTreeItemRenderer extends AbstractProfileResourceTreeRe private readonly hoverDelegate: IHoverDelegate; constructor( - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.labels = instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER); @@ -978,15 +1114,27 @@ class ProfileResourceChildTreeItemRenderer extends AbstractProfileResourceTreeRe const checkbox = disposables.add(new Checkbox('', false, defaultCheckboxStyles)); append(container, checkbox.domNode); const resourceLabel = disposables.add(this.labels.create(container, { hoverDelegate: this.hoverDelegate })); - return { checkbox, resourceLabel, disposables, elementDisposables: disposables.add(new DisposableStore()) }; + + const actionsContainer = append(container, $('.profile-tree-item-actions-container')); + const actionBar = disposables.add(this.instantiationService.createInstance(WorkbenchToolBar, + actionsContainer, + { + hoverDelegate: disposables.add(createInstantHoverDelegate()), + highlightToggledItems: true + } + )); + + return { checkbox, resourceLabel, actionBar, disposables, elementDisposables: disposables.add(new DisposableStore()) }; } renderElement({ element: profileResourceTreeElement }: ITreeNode, index: number, templateData: IProfileResourceChildTreeItemTemplateData, height: number | undefined): void { templateData.elementDisposables.clear(); const { element } = profileResourceTreeElement; - if (isString(element)) { - throw new Error('NewProfileResourceTreeRenderer can only render profile resource child tree items'); + + if (!isProfileResourceChildElement(element)) { + throw new Error('Invalid profile resource element'); } + if (element.checkbox) { templateData.checkbox.domNode.classList.remove('hide'); templateData.checkbox.checked = element.checkbox.isChecked; @@ -998,17 +1146,17 @@ class ProfileResourceChildTreeItemRenderer extends AbstractProfileResourceTreeRe templateData.checkbox.domNode.classList.add('hide'); } - const resource = URI.revive(element.resourceUri); templateData.resourceLabel.setResource( { - name: resource ? basename(resource) : element.label?.label, - description: isString(element.description) ? element.description : undefined, - resource + name: element.resource ? basename(element.resource) : element.label, + resource: element.resource }, { forceLabel: true, - hideIcon: !resource, + icon: element.icon, + hideIcon: !element.resource && !element.icon, }); + templateData.actionBar.setActions(element.action ? [element.action] : []); } } diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts index bde549ecefa..a2e56bc5c7b 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Action, IAction, Separator } from 'vs/base/common/actions'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -25,7 +25,15 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { IFileService } from 'vs/platform/files/common/files'; import { generateUuid } from 'vs/base/common/uuid'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { ITreeItemCheckboxState } from 'vs/workbench/common/views'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { CONFIG_NEW_WINDOW_PROFILE } from 'vs/workbench/common/configuration'; export type ChangeEvent = { readonly name?: boolean; @@ -35,15 +43,33 @@ export type ChangeEvent = { readonly message?: boolean; readonly copyFrom?: boolean; readonly copyFlags?: boolean; + readonly preview?: boolean; + readonly disabled?: boolean; + readonly newWindowProfile?: boolean; }; -export interface IProfileElement { - readonly onDidChange?: Event; - readonly name: string; - readonly icon?: string; - readonly flags?: UseDefaultProfileFlags; - readonly active?: boolean; - readonly message?: string; +export interface IProfileChildElement { + readonly handle: string; + readonly action?: IAction; + readonly checkbox?: ITreeItemCheckboxState; +} + +export interface IProfileResourceTypeElement extends IProfileChildElement { + readonly resourceType: ProfileResourceType; +} + +export interface IProfileResourceTypeChildElement extends IProfileChildElement { + readonly label: string; + readonly resource?: URI; + readonly icon?: ThemeIcon; +} + +export function isProfileResourceTypeElement(element: IProfileChildElement): element is IProfileResourceTypeElement { + return (element as IProfileResourceTypeElement).resourceType !== undefined; +} + +export function isProfileResourceChildElement(element: IProfileChildElement): element is IProfileResourceTypeChildElement { + return (element as IProfileResourceTypeChildElement).label !== undefined; } export abstract class AbstractUserDataProfileElement extends Disposable { @@ -51,12 +77,16 @@ export abstract class AbstractUserDataProfileElement extends Disposable { protected readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; + private readonly saveScheduler = this._register(new RunOnceScheduler(() => this.doSave(), 500)); + constructor( name: string, icon: string | undefined, flags: UseDefaultProfileFlags | undefined, isActive: boolean, + @IUserDataProfileManagementService protected readonly userDataProfileManagementService: IUserDataProfileManagementService, @IUserDataProfilesService protected readonly userDataProfilesService: IUserDataProfilesService, + @ICommandService protected readonly commandService: ICommandService, @IInstantiationService protected readonly instantiationService: IInstantiationService, ) { super(); @@ -68,17 +98,16 @@ export abstract class AbstractUserDataProfileElement extends Disposable { if (!e.message) { this.validate(); } - if (this.primaryAction) { - this.primaryAction.enabled = !this.message; - } + this.save(); })); } private _name = ''; get name(): string { return this._name; } - set name(label: string) { - if (this._name !== label) { - this._name = label; + set name(name: string) { + name = name.trim(); + if (this._name !== name) { + this._name = name; this._onDidChange.fire({ name: true }); } } @@ -119,6 +148,15 @@ export abstract class AbstractUserDataProfileElement extends Disposable { } } + private _disabled: boolean = false; + get disabled(): boolean { return this._disabled; } + set disabled(saving: boolean) { + if (this._disabled !== saving) { + this._disabled = saving; + this._onDidChange.fire({ disabled: true }); + } + } + getFlag(key: ProfileResourceType): boolean { return this.flags?.[key] ?? false; } @@ -151,51 +189,142 @@ export abstract class AbstractUserDataProfileElement extends Disposable { this.message = undefined; } - async getChildren(resourceType: ProfileResourceType): Promise { + async getChildren(resourceType?: ProfileResourceType): Promise { + if (resourceType === undefined) { + const resourceTypes = [ + ProfileResourceType.Settings, + ProfileResourceType.Keybindings, + ProfileResourceType.Tasks, + ProfileResourceType.Snippets, + ProfileResourceType.Extensions + ]; + return Promise.all(resourceTypes.map>(async r => { + const children = (r === ProfileResourceType.Settings + || r === ProfileResourceType.Keybindings + || r === ProfileResourceType.Tasks) ? await this.getChildrenForResourceType(r) : []; + return { + handle: r, + checkbox: undefined, + resourceType: r, + action: children.length + ? new Action('_open', + localize('open', "Open to the Side"), + ThemeIcon.asClassName(Codicon.goToFile), + true, + () => children[0]?.action?.run()) + : undefined + }; + })); + } + return this.getChildrenForResourceType(resourceType); + } + + protected async getChildrenForResourceType(resourceType: ProfileResourceType): Promise { return []; } - protected async getChildrenFromProfile(profile: IUserDataProfile, resourceType: ProfileResourceType): Promise { + protected async getChildrenFromProfile(profile: IUserDataProfile, resourceType: ProfileResourceType): Promise { profile = this.getFlag(resourceType) ? this.userDataProfilesService.defaultProfile : profile; + let children: IProfileResourceChildTreeItem[] = []; switch (resourceType) { case ProfileResourceType.Settings: - return this.instantiationService.createInstance(SettingsResourceTreeItem, profile).getChildren(); + children = await this.instantiationService.createInstance(SettingsResourceTreeItem, profile).getChildren(); + break; case ProfileResourceType.Keybindings: - return this.instantiationService.createInstance(KeybindingsResourceTreeItem, profile).getChildren(); + children = await this.instantiationService.createInstance(KeybindingsResourceTreeItem, profile).getChildren(); + break; case ProfileResourceType.Snippets: - return (await this.instantiationService.createInstance(SnippetsResourceTreeItem, profile).getChildren()) ?? []; + children = (await this.instantiationService.createInstance(SnippetsResourceTreeItem, profile).getChildren()) ?? []; + break; case ProfileResourceType.Tasks: - return this.instantiationService.createInstance(TasksResourceTreeItem, profile).getChildren(); + children = await this.instantiationService.createInstance(TasksResourceTreeItem, profile).getChildren(); + break; case ProfileResourceType.Extensions: - return this.instantiationService.createInstance(ExtensionsResourceExportTreeItem, profile).getChildren(); - } - return []; + children = await this.instantiationService.createInstance(ExtensionsResourceExportTreeItem, profile).getChildren(); + break; + } + return children.map(child => this.toUserDataProfileResourceChildElement(child)); + } + + protected toUserDataProfileResourceChildElement(child: IProfileResourceChildTreeItem): IProfileResourceTypeChildElement { + return { + handle: child.handle, + checkbox: child.checkbox, + label: child.label?.label ?? '', + resource: URI.revive(child.resourceUri), + icon: child.themeIcon, + action: new Action('_openChild', localize('open', "Open to the Side"), ThemeIcon.asClassName(Codicon.goToFile), true, async () => { + if (child.parent.type === ProfileResourceType.Extensions) { + await this.commandService.executeCommand('extension.open', child.handle, undefined, true, undefined, true); + } else if (child.resourceUri) { + await this.commandService.executeCommand(API_OPEN_EDITOR_COMMAND_ID, child.resourceUri, [SIDE_GROUP], undefined); + } + }) + }; + } - protected getInitialName(): string { + getInitialName(): string { return ''; } - abstract readonly primaryAction?: Action; + save(): void { + this.saveScheduler.schedule(); + } + + private hasUnsavedChanges(profile: IUserDataProfile): boolean { + if (this.name !== profile.name) { + return true; + } + if (this.icon !== profile.icon) { + return true; + } + if (!equals(this.flags ?? {}, profile.useDefaultFlags ?? {})) { + return true; + } + return false; + } + + protected async saveProfile(profile: IUserDataProfile): Promise { + if (!this.hasUnsavedChanges(profile)) { + return; + } + this.validate(); + if (this.message) { + return; + } + const useDefaultFlags: UseDefaultProfileFlags | undefined = this.flags + ? this.flags.settings && this.flags.keybindings && this.flags.tasks && this.flags.globalState && this.flags.extensions ? undefined : this.flags + : undefined; + + return await this.userDataProfileManagementService.updateProfile(profile, { + name: this.name, + icon: this.icon, + useDefaultFlags: profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags + }); + } + + abstract readonly titleButtons: [Action[], Action[]]; abstract readonly titleActions: [IAction[], IAction[]]; - abstract readonly contextMenuActions: IAction[]; + abstract readonly actions: [IAction[], IAction[]]; + + protected abstract doSave(): Promise; } -export class UserDataProfileElement extends AbstractUserDataProfileElement implements IProfileElement { +export class UserDataProfileElement extends AbstractUserDataProfileElement { get profile(): IUserDataProfile { return this._profile; } - readonly primaryAction = undefined; - - private readonly saveScheduler = this._register(new RunOnceScheduler(() => this.doSave(), 500)); - constructor( private _profile: IUserDataProfile, + readonly titleButtons: [Action[], Action[]], readonly titleActions: [IAction[], IAction[]], - readonly contextMenuActions: IAction[], + readonly actions: [IAction[], IAction[]], @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, - @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IUserDataProfileManagementService userDataProfileManagementService: IUserDataProfileManagementService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @ICommandService commandService: ICommandService, @IInstantiationService instantiationService: IInstantiationService, ) { super( @@ -203,9 +332,18 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple _profile.icon, _profile.useDefaultFlags, userDataProfileService.currentProfile.id === _profile.id, + userDataProfileManagementService, userDataProfilesService, + commandService, instantiationService, ); + this._isNewWindowProfile = this.configurationService.getValue(CONFIG_NEW_WINDOW_PROFILE) === this.profile.name; + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(CONFIG_NEW_WINDOW_PROFILE)) { + this.isNewWindowProfile = this.configurationService.getValue(CONFIG_NEW_WINDOW_PROFILE) === this.profile.name; + } + } + )); this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => this.active = this.userDataProfileService.currentProfile.id === this.profile.id)); this._register(this.userDataProfilesService.onDidChangeProfiles(() => { const profile = this.userDataProfilesService.profiles.find(p => p.id === this.profile.id); @@ -216,52 +354,34 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple this.flags = profile.useDefaultFlags; } })); - this._register(this.onDidChange(e => { - this.save(); - })); } - private hasUnsavedChanges(): boolean { - if (this.name !== this.profile.name) { - return true; - } - if (this.icon !== this.profile.icon) { - return true; - } - if (!equals(this.flags ?? {}, this.profile.useDefaultFlags ?? {})) { - return true; + public async toggleNewWindowProfile(): Promise { + if (this._isNewWindowProfile) { + await this.configurationService.updateValue(CONFIG_NEW_WINDOW_PROFILE, null); + } else { + await this.configurationService.updateValue(CONFIG_NEW_WINDOW_PROFILE, this.profile.name); } - return false; } - save(): void { - this.saveScheduler.schedule(); - } - - private async doSave(): Promise { - if (!this.hasUnsavedChanges()) { - return; - } - this.validate(); - if (this.message) { - return; + private _isNewWindowProfile: boolean = false; + get isNewWindowProfile(): boolean { return this._isNewWindowProfile; } + set isNewWindowProfile(isNewWindowProfile: boolean) { + if (this._isNewWindowProfile !== isNewWindowProfile) { + this._isNewWindowProfile = isNewWindowProfile; + this._onDidChange.fire({ newWindowProfile: true }); } - const useDefaultFlags: UseDefaultProfileFlags | undefined = this.flags - ? this.flags.settings && this.flags.keybindings && this.flags.tasks && this.flags.globalState && this.flags.extensions ? undefined : this.flags - : undefined; + } - await this.userDataProfileManagementService.updateProfile(this.profile, { - name: this.name, - icon: this.icon, - useDefaultFlags: this.profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags - }); + protected override async doSave(): Promise { + await this.saveProfile(this.profile); } - override async getChildren(resourceType: ProfileResourceType): Promise { + protected override async getChildrenForResourceType(resourceType: ProfileResourceType): Promise { return this.getChildrenFromProfile(this.profile, resourceType); } - protected override getInitialName(): string { + override getInitialName(): string { return this.profile.name; } @@ -269,17 +389,22 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple const USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME = 'userdataprofiletemplatepreview'; -export class NewProfileElement extends AbstractUserDataProfileElement implements IProfileElement { +export class NewProfileElement extends AbstractUserDataProfileElement { + + private templatePromise: CancelablePromise | undefined; + private template: IUserDataProfileTemplate | null = null; constructor( name: string, copyFrom: URI | IUserDataProfile | undefined, - readonly primaryAction: Action, + readonly titleButtons: [Action[], Action[]], readonly titleActions: [IAction[], IAction[]], - readonly contextMenuActions: Action[], + readonly actions: [IAction[], IAction[]], @IFileService private readonly fileService: IFileService, @IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService, + @IUserDataProfileManagementService userDataProfileManagementService: IUserDataProfileManagementService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @ICommandService commandService: ICommandService, @IInstantiationService instantiationService: IInstantiationService, ) { super( @@ -287,11 +412,14 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements undefined, undefined, false, + userDataProfileManagementService, userDataProfilesService, + commandService, instantiationService, ); this._copyFrom = copyFrom; this._copyFlags = this.getCopyFlagsFrom(copyFrom); + this.initialize(); this._register(this.fileService.registerProvider(USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME, this._register(new InMemoryFileSystemProvider()))); } @@ -303,6 +431,11 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements this._onDidChange.fire({ copyFrom: true }); this.flags = undefined; this.copyFlags = this.getCopyFlagsFrom(copyFrom); + if (copyFrom instanceof URI) { + this.templatePromise?.cancel(); + this.templatePromise = undefined; + } + this.initialize(); } } @@ -315,6 +448,15 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements } } + private _previewProfile: IUserDataProfile | undefined; + get previewProfile(): IUserDataProfile | undefined { return this._previewProfile; } + set previewProfile(profile: IUserDataProfile | undefined) { + if (this._previewProfile !== profile) { + this._previewProfile = profile; + this._onDidChange.fire({ preview: true }); + } + } + private getCopyFlagsFrom(copyFrom: URI | IUserDataProfile | undefined): ProfileResourceTypeFlags | undefined { return copyFrom ? { settings: true, @@ -325,6 +467,77 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements } : undefined; } + private async initialize(): Promise { + this.disabled = true; + try { + if (this.copyFrom instanceof URI) { + await this.resolveTemplate(this.copyFrom); + if (this.template) { + this.name = this.template.name ?? ''; + this.icon = this.template.icon; + this.setCopyFlag(ProfileResourceType.Settings, !!this.template.settings); + this.setCopyFlag(ProfileResourceType.Keybindings, !!this.template.keybindings); + this.setCopyFlag(ProfileResourceType.Tasks, !!this.template.tasks); + this.setCopyFlag(ProfileResourceType.Snippets, !!this.template.snippets); + this.setCopyFlag(ProfileResourceType.Extensions, !!this.template.extensions); + } + return; + } + + if (isUserDataProfile(this.copyFrom)) { + this.name = `${this.copyFrom.name} (Copy)`; + this.icon = this.copyFrom.icon; + this.setCopyFlag(ProfileResourceType.Settings, true); + this.setCopyFlag(ProfileResourceType.Keybindings, true); + this.setCopyFlag(ProfileResourceType.Tasks, true); + this.setCopyFlag(ProfileResourceType.Snippets, true); + this.setCopyFlag(ProfileResourceType.Extensions, true); + return; + } + + this.name = localize('untitled', "Untitled"); + this.icon = undefined; + this.setCopyFlag(ProfileResourceType.Settings, false); + this.setCopyFlag(ProfileResourceType.Keybindings, false); + this.setCopyFlag(ProfileResourceType.Tasks, false); + this.setCopyFlag(ProfileResourceType.Snippets, false); + this.setCopyFlag(ProfileResourceType.Extensions, false); + } finally { + this.disabled = false; + } + } + + async resolveTemplate(uri: URI): Promise { + if (!this.templatePromise) { + this.templatePromise = createCancelablePromise(async token => { + const template = await this.userDataProfileImportExportService.resolveProfileTemplate(uri); + if (!token.isCancellationRequested) { + this.template = template; + } + }); + } + await this.templatePromise; + return this.template; + } + + hasResource(resourceType: ProfileResourceType): boolean { + if (this.template) { + switch (resourceType) { + case ProfileResourceType.Settings: + return !!this.template.settings; + case ProfileResourceType.Keybindings: + return !!this.template.keybindings; + case ProfileResourceType.Snippets: + return !!this.template.snippets; + case ProfileResourceType.Tasks: + return !!this.template.tasks; + case ProfileResourceType.Extensions: + return !!this.template.extensions; + } + } + return true; + } + getCopyFlag(key: ProfileResourceType): boolean { return this.copyFlags?.[key] ?? false; } @@ -335,56 +548,85 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements this.copyFlags = flags; } - override async getChildren(resourceType: ProfileResourceType): Promise { + getCopyFromName(): string | undefined { + if (isUserDataProfile(this.copyFrom)) { + return this.copyFrom.name; + } + if (this.template) { + return this.template.name; + } + return undefined; + } + + protected override async getChildrenForResourceType(resourceType: ProfileResourceType): Promise { + if (this.getFlag(resourceType)) { + return this.getChildrenFromProfile(this.userDataProfilesService.defaultProfile, resourceType); + } if (!this.getCopyFlag(resourceType)) { return []; } if (this.copyFrom instanceof URI) { - const template = await this.userDataProfileImportExportService.resolveProfileTemplate(this.copyFrom); - if (!template) { + await this.resolveTemplate(this.copyFrom); + if (!this.template) { return []; } - return this.getChildrenFromProfileTemplate(template, resourceType); + return this.getChildrenFromProfileTemplate(this.template, resourceType); } if (this.copyFrom) { return this.getChildrenFromProfile(this.copyFrom, resourceType); } - if (this.getFlag(resourceType)) { - return this.getChildrenFromProfile(this.userDataProfilesService.defaultProfile, resourceType); - } return []; } - private async getChildrenFromProfileTemplate(profileTemplate: IUserDataProfileTemplate, resourceType: ProfileResourceType): Promise { + private async getChildrenFromProfileTemplate(profileTemplate: IUserDataProfileTemplate, resourceType: ProfileResourceType): Promise { const profile = toUserDataProfile(generateUuid(), this.name, URI.file('/root').with({ scheme: USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME }), URI.file('/cache').with({ scheme: USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME })); switch (resourceType) { case ProfileResourceType.Settings: if (profileTemplate.settings) { await this.instantiationService.createInstance(SettingsResource).apply(profileTemplate.settings, profile); + return this.getChildrenFromProfile(profile, resourceType); } - return this.getChildrenFromProfile(profile, resourceType); + return []; case ProfileResourceType.Keybindings: if (profileTemplate.keybindings) { await this.instantiationService.createInstance(KeybindingsResource).apply(profileTemplate.keybindings, profile); + return this.getChildrenFromProfile(profile, resourceType); } - return this.getChildrenFromProfile(profile, resourceType); + return []; case ProfileResourceType.Snippets: if (profileTemplate.snippets) { await this.instantiationService.createInstance(SnippetsResource).apply(profileTemplate.snippets, profile); + return this.getChildrenFromProfile(profile, resourceType); } - return this.getChildrenFromProfile(profile, resourceType); + return []; case ProfileResourceType.Tasks: if (profileTemplate.tasks) { await this.instantiationService.createInstance(TasksResource).apply(profileTemplate.tasks, profile); + return this.getChildrenFromProfile(profile, resourceType); } - return this.getChildrenFromProfile(profile, resourceType); + return []; case ProfileResourceType.Extensions: if (profileTemplate.extensions) { - return this.instantiationService.createInstance(ExtensionsResourceImportTreeItem, profileTemplate.extensions).getChildren(); + const children = await this.instantiationService.createInstance(ExtensionsResourceImportTreeItem, profileTemplate.extensions).getChildren(); + return children.map(child => this.toUserDataProfileResourceChildElement(child)); } + return []; } return []; } + + override getInitialName(): string { + return this.previewProfile?.name ?? ''; + } + + protected override async doSave(): Promise { + if (this.previewProfile) { + const profile = await this.saveProfile(this.previewProfile); + if (profile) { + this.previewProfile = profile; + } + } + } } export class UserDataProfilesEditorModel extends EditorModel { @@ -430,53 +672,107 @@ export class UserDataProfilesEditorModel extends EditorModel { @IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService, @IDialogService private readonly dialogService: IDialogService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IHostService private readonly hostService: IHostService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); for (const profile of userDataProfilesService.profiles) { - this._profiles.push(this.createProfileElement(profile)); + if (!profile.isTransient) { + this._profiles.push(this.createProfileElement(profile)); + } } this._register(toDisposable(() => this._profiles.splice(0, this._profiles.length).map(([, disposables]) => disposables.dispose()))); this._register(userDataProfilesService.onDidChangeProfiles(e => this.onDidChangeProfiles(e))); } private onDidChangeProfiles(e: DidChangeProfilesEvent): void { + let changed = false; for (const profile of e.added) { - if (profile.name !== this.newProfileElement?.name) { + if (!profile.isTransient && profile.name !== this.newProfileElement?.name) { + changed = true; this._profiles.push(this.createProfileElement(profile)); } } for (const profile of e.removed) { + if (profile.id === this.newProfileElement?.previewProfile?.id) { + this.newProfileElement.previewProfile = undefined; + } const index = this._profiles.findIndex(([p]) => p instanceof UserDataProfileElement && p.profile.id === profile.id); if (index !== -1) { + changed = true; this._profiles.splice(index, 1).map(([, disposables]) => disposables.dispose()); } } - this._onDidChange.fire(undefined); + if (changed) { + this._onDidChange.fire(undefined); + } } private createProfileElement(profile: IUserDataProfile): [UserDataProfileElement, DisposableStore] { const disposables = new DisposableStore(); - const activateAction = disposables.add(new Action('userDataProfile.activate', localize('active', "Activate"), ThemeIcon.asClassName(Codicon.check), true, () => this.userDataProfileManagementService.switchProfile(profile))); - activateAction.checked = this.userDataProfileService.currentProfile.id === profile.id; - disposables.add(this.userDataProfileService.onDidChangeCurrentProfile(() => activateAction.checked = this.userDataProfileService.currentProfile.id === profile.id)); - const copyFromProfileAction = disposables.add(new Action('userDataProfile.copyFromProfile', localize('copyFromProfile', "Save As..."), ThemeIcon.asClassName(Codicon.copy), true, () => this.createNewProfile(profile))); - const exportAction = disposables.add(new Action('userDataProfile.export', localize('export', "Export..."), ThemeIcon.asClassName(Codicon.export), true, () => this.exportProfile(profile))); - const deleteAction = disposables.add(new Action('userDataProfile.delete', localize('delete', "Delete"), ThemeIcon.asClassName(Codicon.trash), true, () => this.removeProfile(profile))); + const activateAction = disposables.add(new Action( + 'userDataProfile.activate', + localize('active', "Use for Current Window"), + ThemeIcon.asClassName(Codicon.check), + true, + () => this.userDataProfileManagementService.switchProfile(profileElement.profile) + )); - const titlePrimaryActions: IAction[] = []; - titlePrimaryActions.push(activateAction); - titlePrimaryActions.push(exportAction); - if (!profile.isDefault) { - titlePrimaryActions.push(deleteAction); - } + const copyFromProfileAction = disposables.add(new Action( + 'userDataProfile.copyFromProfile', + localize('copyFromProfile', "Duplicate..."), + ThemeIcon.asClassName(Codicon.copy), + true, () => this.createNewProfile(profileElement.profile) + )); + + const exportAction = disposables.add(new Action( + 'userDataProfile.export', + localize('export', "Export..."), + ThemeIcon.asClassName(Codicon.export), + true, + () => this.exportProfile(profileElement.profile) + )); + + const deleteAction = disposables.add(new Action( + 'userDataProfile.delete', + localize('delete', "Delete"), + ThemeIcon.asClassName(Codicon.trash), + true, + () => this.removeProfile(profileElement.profile) + )); + const newWindowAction = disposables.add(new Action( + 'userDataProfile.newWindow', + localize('open new window', "Open New Window with this Profile"), + ThemeIcon.asClassName(Codicon.emptyWindow), + true, + () => this.openWindow(profileElement.profile) + )); + + const useAsNewWindowProfileAction = disposables.add(new Action( + 'userDataProfile.useAsNewWindowProfile', + localize('use as new window', "Use for New Windows"), + undefined, + true, + () => profileElement.toggleNewWindowProfile() + )); + + const titlePrimaryActions: IAction[] = []; + titlePrimaryActions.push(newWindowAction); const titleSecondaryActions: IAction[] = []; titleSecondaryActions.push(copyFromProfileAction); + titleSecondaryActions.push(exportAction); + if (!profile.isDefault) { + titleSecondaryActions.push(new Separator()); + titleSecondaryActions.push(deleteAction); + } + const primaryActions: IAction[] = []; + primaryActions.push(newWindowAction); const secondaryActions: IAction[] = []; secondaryActions.push(activateAction); + secondaryActions.push(useAsNewWindowProfileAction); secondaryActions.push(new Separator()); secondaryActions.push(copyFromProfileAction); secondaryActions.push(exportAction); @@ -484,28 +780,81 @@ export class UserDataProfilesEditorModel extends EditorModel { secondaryActions.push(new Separator()); secondaryActions.push(deleteAction); } + const profileElement = disposables.add(this.instantiationService.createInstance(UserDataProfileElement, profile, + [[], []], [titlePrimaryActions, titleSecondaryActions], - secondaryActions, + [primaryActions, secondaryActions] )); + + activateAction.checked = this.userDataProfileService.currentProfile.id === profileElement.profile.id; + disposables.add(this.userDataProfileService.onDidChangeCurrentProfile(() => + activateAction.checked = this.userDataProfileService.currentProfile.id === profileElement.profile.id)); + + useAsNewWindowProfileAction.checked = profileElement.isNewWindowProfile; + disposables.add(profileElement.onDidChange(e => { + if (e.newWindowProfile) { + useAsNewWindowProfileAction.checked = profileElement.isNewWindowProfile; + } + })); + return [profileElement, disposables]; } - createNewProfile(copyFrom?: URI | IUserDataProfile): IProfileElement { + async createNewProfile(copyFrom?: URI | IUserDataProfile): Promise { + if (this.newProfileElement) { + const result = await this.dialogService.confirm({ + type: 'info', + message: localize('new profile exists', "A new profile is already being created. Do you want to discard it and create a new one?"), + primaryButton: localize('discard', "Discard & Create"), + cancelButton: localize('cancel', "Cancel") + }); + if (!result.confirmed) { + return; + } + this.revert(); + } if (!this.newProfileElement) { const disposables = new DisposableStore(); - const discardAction = disposables.add(new Action('userDataProfile.discard', localize('discard', "Discard"), ThemeIcon.asClassName(Codicon.close), true, () => { - this.removeNewProfile(); - this._onDidChange.fire(undefined); - })); + const cancellationTokenSource = new CancellationTokenSource(); + disposables.add(toDisposable(() => cancellationTokenSource.dispose(true))); + const createAction = disposables.add(new Action( + 'userDataProfile.create', + localize('create', "Create"), + undefined, + true, + () => this.saveNewProfile(false, cancellationTokenSource.token) + )); + const cancelAction = disposables.add(new Action( + 'userDataProfile.cancel', + localize('cancel', "Cancel"), + ThemeIcon.asClassName(Codicon.trash), + true, + () => this.discardNewProfile() + )); + const previewProfileAction = disposables.add(new Action( + 'userDataProfile.preview', + localize('preview', "Preview"), + ThemeIcon.asClassName(Codicon.openPreview), + true, + () => this.previewNewProfile(cancellationTokenSource.token) + )); this.newProfileElement = disposables.add(this.instantiationService.createInstance(NewProfileElement, - localize('untitled', "Untitled"), + copyFrom ? '' : localize('untitled', "Untitled"), copyFrom, - disposables.add(new Action('userDataProfile.create', localize('create', "Create & Apply"), undefined, true, () => this.saveNewProfile())), - [[discardAction], []], - [discardAction], + [[createAction], [cancelAction, previewProfileAction]], + [[], []], + [[cancelAction], []], )); + disposables.add(this.newProfileElement.onDidChange(e => { + if (e.preview) { + previewProfileAction.checked = !!this.newProfileElement?.previewProfile; + } + if (e.disabled || e.message) { + previewProfileAction.enabled = createAction.enabled = !this.newProfileElement?.disabled && !this.newProfileElement?.message; + } + })); this._profiles.push([this.newProfileElement, disposables]); this._onDidChange.fire(this.newProfileElement); } @@ -527,45 +876,123 @@ export class UserDataProfilesEditorModel extends EditorModel { } } - async saveNewProfile(): Promise { + private async previewNewProfile(token: CancellationToken): Promise { if (!this.newProfileElement) { return; } + if (this.newProfileElement.previewProfile) { + return; + } + const profile = await this.saveNewProfile(true, token); + if (profile) { + this.newProfileElement.previewProfile = profile; + await this.openWindow(profile); + } + } + + async saveNewProfile(transient?: boolean, token?: CancellationToken): Promise { + if (!this.newProfileElement) { + return undefined; + } + this.newProfileElement.validate(); if (this.newProfileElement.message) { - return; + return undefined; } - const { flags, icon, name, copyFrom } = this.newProfileElement; - const useDefaultFlags: UseDefaultProfileFlags | undefined = flags - ? flags.settings && flags.keybindings && flags.tasks && flags.globalState && flags.extensions ? undefined : flags - : undefined; - type CreateProfileInfoClassification = { - owner: 'sandy081'; - comment: 'Report when profile is about to be created'; - source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Type of profile source' }; - }; - type CreateProfileInfoEvent = { - source: string | undefined; - }; - const createProfileTelemetryData: CreateProfileInfoEvent = { source: copyFrom instanceof URI ? 'template' : isUserDataProfile(copyFrom) ? 'profile' : copyFrom ? 'external' : undefined }; - - if (copyFrom instanceof URI) { - this.telemetryService.publicLog2('userDataProfile.createFromTemplate', createProfileTelemetryData); - await this.userDataProfileImportExportService.importProfile(copyFrom, { mode: 'apply', name: name, useDefaultFlags, icon: icon ? icon : undefined, resourceTypeFlags: this.newProfileElement.copyFlags }); - } else if (isUserDataProfile(copyFrom)) { - this.telemetryService.publicLog2('userDataProfile.createFromProfile', createProfileTelemetryData); - await this.userDataProfileImportExportService.createFromProfile(copyFrom, name, { useDefaultFlags, icon: icon ? icon : undefined, resourceTypeFlags: this.newProfileElement.copyFlags }); - } else { - this.telemetryService.publicLog2('userDataProfile.createEmptyProfile', createProfileTelemetryData); - await this.userDataProfileManagementService.createAndEnterProfile(name, { useDefaultFlags, icon: icon ? icon : undefined }); + this.newProfileElement.disabled = true; + let profile: IUserDataProfile | undefined; + + try { + if (this.newProfileElement.previewProfile) { + if (!transient) { + profile = await this.userDataProfileManagementService.updateProfile(this.newProfileElement.previewProfile, { transient: false }); + } + } + else { + const { flags, icon, name, copyFrom } = this.newProfileElement; + const useDefaultFlags: UseDefaultProfileFlags | undefined = flags + ? flags.settings && flags.keybindings && flags.tasks && flags.globalState && flags.extensions ? undefined : flags + : undefined; + + type CreateProfileInfoClassification = { + owner: 'sandy081'; + comment: 'Report when profile is about to be created'; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Type of profile source' }; + }; + type CreateProfileInfoEvent = { + source: string | undefined; + }; + const createProfileTelemetryData: CreateProfileInfoEvent = { source: copyFrom instanceof URI ? 'template' : isUserDataProfile(copyFrom) ? 'profile' : copyFrom ? 'external' : undefined }; + + if (copyFrom instanceof URI) { + const template = await this.newProfileElement.resolveTemplate(copyFrom); + if (template) { + this.telemetryService.publicLog2('userDataProfile.createFromTemplate', createProfileTelemetryData); + profile = await this.userDataProfileImportExportService.createProfileFromTemplate( + template, + { + name, + useDefaultFlags, + icon, + resourceTypeFlags: this.newProfileElement.copyFlags, + transient + }, + token ?? CancellationToken.None + ); + } + } else if (isUserDataProfile(copyFrom)) { + this.telemetryService.publicLog2('userDataProfile.createFromProfile', createProfileTelemetryData); + profile = await this.userDataProfileImportExportService.createFromProfile( + copyFrom, + { + name, + useDefaultFlags, + icon: icon, + resourceTypeFlags: this.newProfileElement.copyFlags, + transient + }, + token ?? CancellationToken.None + ); + } else { + this.telemetryService.publicLog2('userDataProfile.createEmptyProfile', createProfileTelemetryData); + profile = await this.userDataProfileManagementService.createProfile(name, { useDefaultFlags, icon, transient }); + } + } + } finally { + if (this.newProfileElement) { + this.newProfileElement.disabled = false; + } } - this.removeNewProfile(); - const profile = this.userDataProfilesService.profiles.find(p => p.name === name); - if (profile) { + if (token?.isCancellationRequested) { + if (profile) { + try { + await this.userDataProfileManagementService.removeProfile(profile); + } catch (error) { + // ignore + } + } + return; + } + + if (profile && !profile.isTransient && this.newProfileElement) { + this.removeNewProfile(); this.onDidChangeProfiles({ added: [profile], removed: [], updated: [], all: this.userDataProfilesService.profiles }); } + + return profile; + } + + private async discardNewProfile(): Promise { + if (!this.newProfileElement) { + return; + } + if (this.newProfileElement.previewProfile) { + await this.userDataProfileManagementService.removeProfile(this.newProfileElement.previewProfile); + } + this.removeNewProfile(); + this._onDidChange.fire(undefined); } private async removeProfile(profile: IUserDataProfile): Promise { @@ -580,7 +1007,11 @@ export class UserDataProfilesEditorModel extends EditorModel { } } + private async openWindow(profile: IUserDataProfile): Promise { + await this.hostService.openWindow({ forceProfile: profile.name }); + } + private async exportProfile(profile: IUserDataProfile): Promise { - return this.userDataProfileImportExportService.exportProfile2(profile); + return this.userDataProfileImportExportService.exportProfile(profile); } } diff --git a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index 5e094fc4ccc..ae571f8c84d 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html @@ -973,7 +973,7 @@ const previousPendingFrame = getPendingFrame(); if (previousPendingFrame) { previousPendingFrame.setAttribute('id', ''); - document.body.removeChild(previousPendingFrame); + previousPendingFrame.remove(); } if (!wasFirstLoad) { pendingMessages = []; @@ -1070,9 +1070,7 @@ if (newFrame && newFrame.contentDocument && newFrame.contentDocument === contentDocument) { const wasFocused = document.hasFocus(); const oldActiveFrame = getActiveFrame(); - if (oldActiveFrame) { - document.body.removeChild(oldActiveFrame); - } + oldActiveFrame?.remove(); // Styles may have changed since we created the element. Make sure we re-style if (initialStyleVersion !== styleVersion) { applyStyles(newFrame.contentDocument, newFrame.contentDocument.body); diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index fa7b15e39c8..f46e1240428 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -5,7 +5,9 @@ + + content="default-src 'none'; script-src 'sha256-ikaxwm2UFoiIKkEZTEU4mnSxpYf3lmsrhy5KqqJZfek=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> + a { + text-decoration: var(--text-link-decoration); + } + a:hover { color: var(--vscode-textLink-activeForeground); } @@ -791,6 +797,17 @@ } } + + function handleInnerDragEvent(/** @type {DragEvent} */ e) { + if (!e.dataTransfer) { + return; + } + + hostMessaging.postMessage('drag', { + shiftKey: e.shiftKey + }); + } + /** * @param {() => void} callback */ @@ -881,7 +898,9 @@ window.addEventListener('keydown', handleInnerKeydown); window.addEventListener('keyup', handleInnerKeyup); window.addEventListener('dragenter', handleInnerDragStartEvent); - window.addEventListener('dragover', handleInnerDragStartEvent); + window.addEventListener('dragover', handleInnerDragEvent); + window.addEventListener('drag', handleInnerDragEvent); + onDomReady(() => { if (!document.body) { @@ -974,7 +993,7 @@ const previousPendingFrame = getPendingFrame(); if (previousPendingFrame) { previousPendingFrame.setAttribute('id', ''); - document.body.removeChild(previousPendingFrame); + previousPendingFrame.remove(); } if (!wasFirstLoad) { pendingMessages = []; @@ -1071,9 +1090,7 @@ if (newFrame && newFrame.contentDocument && newFrame.contentDocument === contentDocument) { const wasFocused = document.hasFocus(); const oldActiveFrame = getActiveFrame(); - if (oldActiveFrame) { - document.body.removeChild(oldActiveFrame); - } + oldActiveFrame?.remove(); // Styles may have changed since we created the element. Make sure we re-style if (initialStyleVersion !== styleVersion) { applyStyles(newFrame.contentDocument, newFrame.contentDocument.body); @@ -1165,7 +1182,8 @@ }); contentWindow.addEventListener('dragenter', handleInnerDragStartEvent); - contentWindow.addEventListener('dragover', handleInnerDragStartEvent); + contentWindow.addEventListener('dragover', handleInnerDragEvent); + contentWindow.addEventListener('drag', handleInnerDragEvent); unloadMonitor.onIframeLoaded(newFrame); } diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index 55cf4daeecd..42dcb6f93ba 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ // @ts-check -/// /// const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {any} */ (self)); @@ -168,7 +167,7 @@ sw.addEventListener('message', async (event) => { sw.addEventListener('fetch', (event) => { const requestUrl = new URL(event.request.url); - if (requestUrl.protocol === 'https:' && requestUrl.hostname.endsWith('.' + resourceBaseAuthority)) { + if (typeof resourceBaseAuthority === 'string' && requestUrl.protocol === 'https:' && requestUrl.hostname.endsWith('.' + resourceBaseAuthority)) { switch (event.request.method) { case 'GET': case 'HEAD': { diff --git a/src/vs/workbench/contrib/webview/browser/themeing.ts b/src/vs/workbench/contrib/webview/browser/themeing.ts index eda7179665f..75ee4b73061 100644 --- a/src/vs/workbench/contrib/webview/browser/themeing.ts +++ b/src/vs/workbench/contrib/webview/browser/themeing.ts @@ -37,7 +37,7 @@ export class WebviewThemeDataProvider extends Disposable { this._reset(); })); - const webviewConfigurationKeys = ['editor.fontFamily', 'editor.fontWeight', 'editor.fontSize']; + const webviewConfigurationKeys = ['editor.fontFamily', 'editor.fontWeight', 'editor.fontSize', 'accessibility.underlineLinks']; this._register(this._configurationService.onDidChangeConfiguration(e => { if (webviewConfigurationKeys.some(key => e.affectsConfiguration(key))) { this._reset(); @@ -55,6 +55,7 @@ export class WebviewThemeDataProvider extends Disposable { const editorFontFamily = configuration.fontFamily || EDITOR_FONT_DEFAULTS.fontFamily; const editorFontWeight = configuration.fontWeight || EDITOR_FONT_DEFAULTS.fontWeight; const editorFontSize = configuration.fontSize || EDITOR_FONT_DEFAULTS.fontSize; + const linkUnderlines = this._configurationService.getValue('accessibility.underlineLinks'); const theme = this._themeService.getColorTheme(); const exportedColors = colorRegistry.getColorRegistry().getColors().reduce>((colors, entry) => { @@ -72,6 +73,7 @@ export class WebviewThemeDataProvider extends Disposable { 'vscode-editor-font-family': editorFontFamily, 'vscode-editor-font-weight': editorFontWeight, 'vscode-editor-font-size': editorFontSize + 'px', + 'text-link-decoration': linkUnderlines ? 'underline' : 'none', ...exportedColors }; diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index ed368a152c5..230cafaf15c 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -34,7 +34,7 @@ import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { areWebviewContentOptionsEqual, IWebview, WebviewContentOptions, WebviewExtensionDescription, WebviewInitInfo, WebviewMessageReceivedEvent, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewFindDelegate, WebviewFindWidget } from 'vs/workbench/contrib/webview/browser/webviewFindWidget'; -import { FromWebviewMessage, KeyEvent, ToWebviewMessage } from 'vs/workbench/contrib/webview/browser/webviewMessages'; +import { FromWebviewMessage, KeyEvent, ToWebviewMessage, WebViewDragEvent } from 'vs/workbench/contrib/webview/browser/webviewMessages'; import { decodeAuthority, webviewGenericCspSource, webviewRootResourceAuthority } from 'vs/workbench/contrib/webview/common/webview'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { CodeWindow } from 'vs/base/browser/window'; @@ -325,6 +325,10 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this._startBlockingIframeDragEvents(); })); + this._register(this.on('drag', (event) => { + this.handleDragEvent('drag', event); + })); + if (initInfo.options.enableFindWidget) { // --- Start Positron --- // Added this._options.webviewFindDelegate || this so that if a WebviewFindDelegate is @@ -795,6 +799,17 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this.window?.dispatchEvent(emulatedKeyboardEvent); } + private handleDragEvent(type: 'drag', event: WebViewDragEvent) { + // Create a fake DragEvent from the data provided + const emulatedDragEvent = new DragEvent(type, event); + // Force override the target + Object.defineProperty(emulatedDragEvent, 'target', { + get: () => this.element, + }); + // And re-dispatch + this.window?.dispatchEvent(emulatedDragEvent); + } + windowDidDragStart(): void { // Webview break drag and dropping around the main window (no events are generated when you are over them) // Work around this by disabling pointer events during the drag. diff --git a/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts b/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts index 837b15049a9..e8d58d50f1b 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts @@ -17,6 +17,10 @@ type KeyEvent = { repeat: boolean; } +type WebViewDragEvent = { + shiftKey: boolean; +} + export type FromWebviewMessage = { 'onmessage': { message: any; transfer?: ArrayBuffer[] }; 'did-click-link': { uri: string }; @@ -36,6 +40,7 @@ export type FromWebviewMessage = { 'did-keyup': KeyEvent; 'did-context-menu': { clientX: number; clientY: number; context: { [key: string]: unknown } }; 'drag-start': void; + 'drag': WebViewDragEvent }; interface UpdateContentEvent { diff --git a/src/vs/workbench/contrib/webview/browser/webviewWindowDragMonitor.ts b/src/vs/workbench/contrib/webview/browser/webviewWindowDragMonitor.ts index e4dc5eaf0e9..d009ae186da 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewWindowDragMonitor.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewWindowDragMonitor.ts @@ -18,19 +18,41 @@ export class WebviewWindowDragMonitor extends Disposable { constructor(targetWindow: CodeWindow, getWebview: () => IWebview | undefined) { super(); - this._register(DOM.addDisposableListener(targetWindow, DOM.EventType.DRAG_START, () => { + const onDragStart = () => { getWebview()?.windowDidDragStart(); - })); + }; const onDragEnd = () => { getWebview()?.windowDidDragEnd(); }; + this._register(DOM.addDisposableListener(targetWindow, DOM.EventType.DRAG_START, () => { + onDragStart(); + })); + this._register(DOM.addDisposableListener(targetWindow, DOM.EventType.DRAG_END, onDragEnd)); + this._register(DOM.addDisposableListener(targetWindow, DOM.EventType.MOUSE_MOVE, currentEvent => { if (currentEvent.buttons === 0) { onDragEnd(); } })); + + this._register(DOM.addDisposableListener(targetWindow, DOM.EventType.DRAG, (event) => { + if (event.shiftKey) { + onDragEnd(); + } else { + onDragStart(); + } + })); + + this._register(DOM.addDisposableListener(targetWindow, DOM.EventType.DRAG_OVER, (event) => { + if (event.shiftKey) { + onDragEnd(); + } else { + onDragStart(); + } + })); + } } diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts index f24731711d3..3777d363bbf 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -14,34 +14,31 @@ export interface WebviewIcons { readonly dark: URI; } -export class WebviewIconManager implements IDisposable { +export class WebviewIconManager extends Disposable { private readonly _icons = new Map(); private _styleElement: HTMLStyleElement | undefined; - private _styleElementDisposable: DisposableStore | undefined; constructor( @ILifecycleService private readonly _lifecycleService: ILifecycleService, @IConfigurationService private readonly _configService: IConfigurationService, ) { - this._configService.onDidChangeConfiguration(e => { + super(); + this._register(this._configService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('workbench.iconTheme')) { this.updateStyleSheet(); } - }); + })); } - - dispose() { - this._styleElementDisposable?.dispose(); - this._styleElementDisposable = undefined; + override dispose() { + super.dispose(); this._styleElement = undefined; } private get styleElement(): HTMLStyleElement { if (!this._styleElement) { - this._styleElementDisposable = new DisposableStore(); - this._styleElement = dom.createStyleSheet(undefined, undefined, this._styleElementDisposable); + this._styleElement = dom.createStyleSheet(undefined, undefined, this._store); this._styleElement.className = 'webview-icons'; } return this._styleElement; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 7e90babf753..e910f1569e5 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -72,8 +72,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { GettingStartedIndexList } from './gettingStartedList'; -/*--- Start Positron ---*/ -// import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; +// --- Start Positron --- import 'vs/css!./media/positronGettingStarted'; import { PositronReactRenderer } from 'vs/base/browser/positronReactRenderer'; import { createWelcomePageLeft } from 'vs/workbench/contrib/welcomeGettingStarted/browser/positronWelcomePageLeft'; @@ -82,7 +81,7 @@ import { IRuntimeSessionService } from 'vs/workbench/services/runtimeSession/com import { IRuntimeStartupService } from 'vs/workbench/services/runtimeStartup/common/runtimeStartupService'; import { ILanguageRuntimeService } from 'vs/workbench/services/languageRuntime/common/languageRuntimeService'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -/*--- End Positron ---*/ +// --- End Positron --- const SLIDE_TRANSITION_TIME_MS = 250; const configurationKey = 'workbench.startupEditor'; @@ -160,7 +159,6 @@ export class GettingStartedPage extends EditorPane { private recentlyOpenedList?: GettingStartedIndexList; private startList?: GettingStartedIndexList; private gettingStartedList?: GettingStartedIndexList; - private videoList?: GettingStartedIndexList; private stepsSlide!: HTMLElement; private categoriesSlide!: HTMLElement; @@ -206,7 +204,6 @@ export class GettingStartedPage extends EditorPane { @IRuntimeStartupService private readonly runtimeStartupService: IRuntimeStartupService, @ILanguageRuntimeService private readonly languageRuntimeService: ILanguageRuntimeService, @ILifecycleService private readonly lifecycleService: ILifecycleService, - // @IWorkbenchAssignmentService private readonly tasExperimentService: IWorkbenchAssignmentService ) { super(GettingStartedPage.ID, group, telemetryService, themeService, storageService); @@ -469,10 +466,6 @@ export class GettingStartedPage extends EditorPane { } break; } - case 'hideVideos': { - this.hideVideos(); - break; - } case 'openLink': { this.openerService.open(argument); break; @@ -491,11 +484,6 @@ export class GettingStartedPage extends EditorPane { this.gettingStartedList?.rerender(); } - private hideVideos() { - this.setHiddenCategories([...this.getHiddenCategories().add('getting-started-videos')]); - this.videoList?.setEntries(undefined); - } - private markAllStepsComplete() { if (this.currentWalkthrough) { this.currentWalkthrough?.steps.forEach(step => { @@ -855,6 +843,7 @@ export class GettingStartedPage extends EditorPane { const startList = this.buildStartList(); const recentList = this.buildRecentlyOpenedList(); + // const gettingStartedList = this.buildGettingStartedWalkthroughsList(); const footer = $('.footer', {}, $('p.showOnStartup', {}, @@ -1001,6 +990,101 @@ export class GettingStartedPage extends EditorPane { return startList; } + // --- Start Positron --- + // + // This function is not used in Positron. It is commented out rather than + // being deleted to minimize merge conflicts. + /* + private buildGettingStartedWalkthroughsList(): GettingStartedIndexList { + + const renderGetttingStaredWalkthrough = (category: IResolvedWalkthrough): HTMLElement => { + + const renderNewBadge = (category.newItems || category.newEntry) && !category.isFeatured; + const newBadge = $('.new-badge', {}); + if (category.newEntry) { + reset(newBadge, $('.new-category', {}, localize('new', "New"))); + } else if (category.newItems) { + reset(newBadge, $('.new-items', {}, localize({ key: 'newItems', comment: ['Shown when a list of items has changed based on an update from a remote source'] }, "Updated"))); + } + + const featuredBadge = $('.featured-badge', {}); + const descriptionContent = $('.description-content', {},); + + if (category.isFeatured && this.showFeaturedWalkthrough) { + reset(featuredBadge, $('.featured', {}, $('span.featured-icon.codicon.codicon-star-full'))); + reset(descriptionContent, ...renderLabelWithIcons(category.description)); + } + + const titleContent = $('h3.category-title.max-lines-3', { 'x-category-title-for': category.id }); + reset(titleContent, ...renderLabelWithIcons(category.title)); + + return $('button.getting-started-category' + (category.isFeatured && this.showFeaturedWalkthrough ? '.featured' : ''), + { + 'x-dispatch': 'selectCategory:' + category.id, + 'title': category.description + }, + featuredBadge, + $('.main-content', {}, + this.iconWidgetFor(category), + titleContent, + renderNewBadge ? newBadge : $('.no-badge'), + $('a.codicon.codicon-close.hide-category-button', { + 'tabindex': 0, + 'x-dispatch': 'hideCategory:' + category.id, + 'title': localize('close', "Hide"), + 'role': 'button', + 'aria-label': localize('closeAriaLabel', "Hide"), + }), + ), + descriptionContent, + $('.category-progress', { 'x-data-category-id': category.id, }, + $('.progress-bar-outer', { 'role': 'progressbar' }, + $('.progress-bar-inner')))); + }; + + if (this.gettingStartedList) { this.gettingStartedList.dispose(); } + + const rankWalkthrough = (e: IResolvedWalkthrough) => { + let rank: number | null = e.order; + + if (e.isFeatured) { rank += 7; } + if (e.newEntry) { rank += 3; } + if (e.newItems) { rank += 2; } + if (e.recencyBonus) { rank += 4 * e.recencyBonus; } + + if (this.getHiddenCategories().has(e.id)) { rank = null; } + return rank; + }; + + const gettingStartedList = this.gettingStartedList = new GettingStartedIndexList( + { + title: localize('walkthroughs', "Walkthroughs"), + klass: 'getting-started', + limit: 5, + footer: $('span.button-link.see-all-walkthroughs', { 'x-dispatch': 'seeAllWalkthroughs', 'tabindex': 0 }, localize('showAll', "More...")), + renderElement: renderGetttingStaredWalkthrough, + rankElement: rankWalkthrough, + contextService: this.contextService, + }); + + gettingStartedList.onDidChange(() => { + const hidden = this.getHiddenCategories(); + const someWalkthroughsHidden = hidden.size || gettingStartedList.itemCount < this.gettingStartedCategories.filter(c => this.contextService.contextMatchesRules(c.when)).length; + this.container.classList.toggle('someWalkthroughsHidden', !!someWalkthroughsHidden); + this.registerDispatchListeners(); + allWalkthroughsHiddenContext.bindTo(this.contextService).set(gettingStartedList.itemCount === 0); + this.updateCategoryProgress(); + }); + + gettingStartedList.setEntries(this.gettingStartedCategories); + allWalkthroughsHiddenContext.bindTo(this.contextService).set(gettingStartedList.itemCount === 0); + + return gettingStartedList; + } + + */ + // --- End Positron --- + layout(size: Dimension) { this.detailsScrollbar?.scanDomNode(); @@ -1010,7 +1094,6 @@ export class GettingStartedPage extends EditorPane { this.startList?.layout(size); this.gettingStartedList?.layout(size); this.recentlyOpenedList?.layout(size); - this.videoList?.layout(size); if (this.editorInput?.selectedStep && this.currentMediaType) { this.mediaDisposables.clear(); @@ -1179,7 +1262,7 @@ export class GettingStartedPage extends EditorPane { } private buildMarkdownDescription(container: HTMLElement, text: LinkedText[]) { - while (container.firstChild) { container.removeChild(container.firstChild); } + while (container.firstChild) { container.firstChild.remove(); } for (const linkedText of text) { if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors.ts index 8e934b891a3..e3227a80af4 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors.ts @@ -7,14 +7,14 @@ import { darken, inputBackground, editorWidgetBackground, lighten, registerColor import { localize } from 'vs/nls'; // Seprate from main module to break dependency cycles between welcomePage and gettingStarted. -export const welcomePageBackground = registerColor('welcomePage.background', { light: null, dark: null, hcDark: null, hcLight: null }, localize('welcomePage.background', 'Background color for the Welcome page.')); +export const welcomePageBackground = registerColor('welcomePage.background', null, localize('welcomePage.background', 'Background color for the Welcome page.')); export const welcomePageTileBackground = registerColor('welcomePage.tileBackground', { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: '#000', hcLight: editorWidgetBackground }, localize('welcomePage.tileBackground', 'Background color for the tiles on the Welcome page.')); export const welcomePageTileHoverBackground = registerColor('welcomePage.tileHoverBackground', { dark: lighten(editorWidgetBackground, .2), light: darken(editorWidgetBackground, .1), hcDark: null, hcLight: null }, localize('welcomePage.tileHoverBackground', 'Hover background color for the tiles on the Welcome.')); export const welcomePageTileBorder = registerColor('welcomePage.tileBorder', { dark: '#ffffff1a', light: '#0000001a', hcDark: contrastBorder, hcLight: contrastBorder }, localize('welcomePage.tileBorder', 'Border color for the tiles on the Welcome page.')); -export const welcomePageProgressBackground = registerColor('welcomePage.progress.background', { light: inputBackground, dark: inputBackground, hcDark: inputBackground, hcLight: inputBackground }, localize('welcomePage.progress.background', 'Foreground color for the Welcome page progress bars.')); -export const welcomePageProgressForeground = registerColor('welcomePage.progress.foreground', { light: textLinkForeground, dark: textLinkForeground, hcDark: textLinkForeground, hcLight: textLinkForeground }, localize('welcomePage.progress.foreground', 'Background color for the Welcome page progress bars.')); +export const welcomePageProgressBackground = registerColor('welcomePage.progress.background', inputBackground, localize('welcomePage.progress.background', 'Foreground color for the Welcome page progress bars.')); +export const welcomePageProgressForeground = registerColor('welcomePage.progress.foreground', textLinkForeground, localize('welcomePage.progress.foreground', 'Background color for the Welcome page progress bars.')); export const walkthroughStepTitleForeground = registerColor('walkthrough.stepTitle.foreground', { light: '#000000', dark: '#ffffff', hcDark: null, hcLight: null }, localize('walkthrough.stepTitle.foreground', 'Foreground color of the heading of each walkthrough step')); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts index 869f44526b4..fef4b4928bf 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts @@ -211,7 +211,7 @@ export class GettingStartedDetailsRenderer { private async readAndCacheStepMarkdown(path: URI, base: URI): Promise { if (!this.mdCache.has(path)) { const contents = await this.readContentsOfPath(path); - const markdownContents = await renderMarkdownDocument(transformUris(contents, base), this.extensionService, this.languageService, true, true); + const markdownContents = await renderMarkdownDocument(transformUris(contents, base), this.extensionService, this.languageService, { allowUnknownProtocols: true }); this.mdCache.set(path, markdownContents); } return assertIsDefined(this.mdCache.get(path)); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedList.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedList.ts index 50144d87e1c..caab78fd561 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedList.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedList.ts @@ -123,7 +123,7 @@ export class GettingStartedIndexList .content .gettingStartedContainer .icon-widget, -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideCategories .icon-widget:not(.codicon-device-camera-video), .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideCategories .featured-icon { font-size: 20px; padding-right: 8px; @@ -238,13 +237,6 @@ top: 3px; } -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideCategories .icon-widget.codicon-device-camera-video { - font-size: 20px; - padding-right: 8px; - position: relative; - transform: translateY(+100%); -} - .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideCategories .codicon:not(.icon-widget, .featured-icon, .hide-category-button) { margin: 0 2px; } @@ -350,7 +342,7 @@ right: 8px; } -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category.featured .icon-widget:not(.codicon-device-camera-video) { +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category.featured .icon-widget { visibility: hidden; } @@ -933,6 +925,7 @@ .monaco-workbench .part.editor > .content .gettingStartedContainer .button-link { color: var(--vscode-textLink-foreground); + text-decoration: var(--text-link-decoration); } .monaco-workbench .part.editor > .content .gettingStartedContainer .button-link .codicon { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts b/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts index b7b09ca9417..3a63341f1d8 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileAccess } from 'vs/base/common/network'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguageService } from 'vs/editor/common/services/languageService'; diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css b/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css index 12efdd7b3f9..25f6dc00afb 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css @@ -18,7 +18,7 @@ } .monaco-workbench .part.editor > .content .walkThroughContent a { - text-decoration: none; + text-decoration: var(--text-link-decoration); } .monaco-workbench .part.editor > .content .walkThroughContent a:focus, @@ -153,6 +153,8 @@ .monaco-workbench .part.editor > .content .walkThroughContent code, .monaco-workbench .part.editor > .content .walkThroughContent .shortcut { color: var(--vscode-textPreformat-foreground); + background-color: var(--vscode-textPreformat-background); + border-radius: 3px; } .monaco-workbench .part.editor > .content .walkThroughContent .monaco-editor { diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts index c2c1e099961..34efe4d154e 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts @@ -414,7 +414,7 @@ export class WalkThroughPart extends EditorPane { const keybinding = command && this.keybindingService.lookupKeybinding(command); const label = keybinding ? keybinding.getLabel() || '' : UNBOUND_COMMAND; while (key.firstChild) { - key.removeChild(key.firstChild); + key.firstChild.remove(); } key.appendChild(document.createTextNode(label)); }); @@ -433,7 +433,7 @@ export class WalkThroughPart extends EditorPane { const keys = this.content.querySelectorAll('.multi-cursor-modifier'); Array.prototype.forEach.call(keys, (key: Element) => { while (key.firstChild) { - key.removeChild(key.firstChild); + key.firstChild.remove(); } key.appendChild(document.createTextNode(modifier)); }); diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 57fe4e9f040..3e07de539b8 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -226,7 +226,7 @@ import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from 'vs/platform/window/electron-sand 'type': 'boolean', 'default': false, 'scope': ConfigurationScope.APPLICATION, - 'markdownDescription': localize('window.doubleClickIconToClose', "If enabled, this setting will close the window when the application icon in the title bar is double-clicked. The window will not be able to be dragged by the icon. This setting is effective only if `#window.titleBarStyle#` is set to `custom`.") + 'markdownDescription': localize('window.doubleClickIconToClose', "If enabled, this setting will close the window when the application icon in the title bar is double-clicked. The window will not be able to be dragged by the icon. This setting is effective only if {0} is set to `custom`.", '`#window.titleBarStyle#`') }, 'window.titleBarStyle': { 'type': 'string', @@ -241,11 +241,11 @@ import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from 'vs/platform/window/electron-sand 'markdownEnumDescriptions': [ localize(`window.customTitleBarVisibility.auto`, "Automatically changes custom title bar visibility."), localize(`window.customTitleBarVisibility.windowed`, "Hide custom titlebar in full screen. When not in full screen, automatically change custom title bar visibility."), - localize(`window.customTitleBarVisibility.never`, "Hide custom titlebar when `#window.titleBarStyle#` is set to `native`."), + localize(`window.customTitleBarVisibility.never`, "Hide custom titlebar when {0} is set to `native`.", '`#window.titleBarStyle#`'), ], 'default': isLinux ? 'never' : 'auto', 'scope': ConfigurationScope.APPLICATION, - 'markdownDescription': localize('window.customTitleBarVisibility', "Adjust when the custom title bar should be shown. The custom title bar can be hidden when in full screen mode with `windowed`. The custom title bar can only be hidden in none full screen mode with `never` when `#window.titleBarStyle#` is set to `native`."), + 'markdownDescription': localize('window.customTitleBarVisibility', "Adjust when the custom title bar should be shown. The custom title bar can be hidden when in full screen mode with `windowed`. The custom title bar can only be hidden in none full screen mode with `never` when {0} is set to `native`.", '`#window.titleBarStyle#`'), }, 'window.dialogStyle': { 'type': 'string', @@ -363,6 +363,10 @@ import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from 'vs/platform/window/electron-sand type: 'boolean', description: localize('argv.disableLcdText', 'Disables LCD font antialiasing.') }, + 'proxy-bypass-list': { + type: 'string', + description: localize('argv.proxyBypassList', 'Bypass any specified proxy for the given semi-colon-separated list of hosts. Example value ";*.microsoft.com;*foo.com;1.2.3.4:5678", will use the proxy server for all hosts except for local addresses (localhost, 127.0.0.1 etc.), microsoft.com subdomains, hosts that contain the suffix foo.com and anything at 1.2.3.4:5678') + }, 'disable-hardware-acceleration': { type: 'boolean', description: localize('argv.disableHardwareAcceleration', 'Disables hardware acceleration. ONLY change this option if you encounter graphic issues.') diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 73ebdb1b2a9..7d8a1901d7a 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -204,7 +204,10 @@ export class NativeWindow extends BaseWindow { [{ label: localize('restart', "Restart"), run: () => this.nativeHostService.relaunch() - }] + }], + { + priority: NotificationPriority.URGENT + } ); }); @@ -248,7 +251,7 @@ export class NativeWindow extends BaseWindow { ); }); - ipcRenderer.on('vscode:showTranslatedBuildWarning', (event: unknown, message: string) => { + ipcRenderer.on('vscode:showTranslatedBuildWarning', () => { this.notificationService.prompt( Severity.Warning, localize("runningTranslated", "You are running an emulated version of {0}. For better performance download the native arm64 version of {0} build for your machine.", this.productService.nameLong), @@ -260,7 +263,24 @@ export class NativeWindow extends BaseWindow { const insidersURL = 'https://code.visualstudio.com/docs/?dv=osx&build=insiders'; this.openerService.open(quality === 'stable' ? stableURL : insidersURL); } - }] + }], + { + priority: NotificationPriority.URGENT + } + ); + }); + + ipcRenderer.on('vscode:showArgvParseWarning', (event: unknown, message: string) => { + this.notificationService.prompt( + Severity.Warning, + localize("showArgvParseWarning", "The runtime arguments file 'argv.json' contains errors. Please correct them and restart."), + [{ + label: localize('showArgvParseWarningAction', "Open File"), + run: () => this.editorService.openEditor({ resource: this.nativeEnvironmentService.argvResource }) + }], + { + priority: NotificationPriority.URGENT + } ); }); diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 0d9a2f85d7a..8c6cdfc4590 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -15,7 +15,6 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import { index } from 'vs/base/common/arrays'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; -import { ApiProposalName } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { IExtensionFeatureTableRenderer, IExtensionFeaturesRegistry, IRenderedData, IRowData, ITableData, Extensions as ExtensionFeaturesExtensions } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { IExtensionManifest, IKeyBinding } from 'vs/platform/extensions/common/extensions'; @@ -25,6 +24,7 @@ import { platform } from 'vs/base/common/process'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ApiProposalName } from 'vs/platform/extensions/common/extensionsApiProposals'; // --- Start Positron --- // TODO(seem): We can remove this if we eventually decide to unbundle vscode-jupyter. diff --git a/src/vs/workbench/services/aiRelatedInformation/test/common/aiRelatedInformationService.test.ts b/src/vs/workbench/services/aiRelatedInformation/test/common/aiRelatedInformationService.test.ts index 52e00d57f7b..db9b4febfbd 100644 --- a/src/vs/workbench/services/aiRelatedInformation/test/common/aiRelatedInformationService.test.ts +++ b/src/vs/workbench/services/aiRelatedInformation/test/common/aiRelatedInformationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { AiRelatedInformationService } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformationService'; import { NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 84cc7730f4b..d154337b9a5 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -12,7 +12,7 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { IProductService } from 'vs/platform/product/common/productService'; import { ISecretStorageService } from 'vs/platform/secrets/common/secrets'; import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; -import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionAccount, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -164,10 +164,10 @@ export class AuthenticationService extends Disposable implements IAuthentication throw new Error(`No authentication provider '${id}' is currently registered.`); } - async getSessions(id: string, scopes?: string[], activateImmediate: boolean = false): Promise> { + async getSessions(id: string, scopes?: string[], account?: AuthenticationSessionAccount, activateImmediate: boolean = false): Promise> { const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate); if (authProvider) { - return await authProvider.getSessions(scopes); + return await authProvider.getSessions(scopes, { account }); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); } @@ -177,7 +177,7 @@ export class AuthenticationService extends Disposable implements IAuthentication const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, !!options?.activateImmediate); if (authProvider) { return await authProvider.createSession(scopes, { - sessionToRecreate: options?.sessionToRecreate + account: options?.account }); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); diff --git a/src/vs/workbench/services/authentication/common/authentication.ts b/src/vs/workbench/services/authentication/common/authentication.ts index 6da6e530237..1d99ffc059a 100644 --- a/src/vs/workbench/services/authentication/common/authentication.ts +++ b/src/vs/workbench/services/authentication/common/authentication.ts @@ -35,8 +35,12 @@ export interface AuthenticationProviderInformation { } export interface IAuthenticationCreateSessionOptions { - sessionToRecreate?: AuthenticationSession; activateImmediate?: boolean; + /** + * The account that is being asked about. If this is passed in, the provider should + * attempt to return the sessions that are only related to this account. + */ + account?: AuthenticationSessionAccount; } export interface AllowedExtension { @@ -131,7 +135,7 @@ export interface IAuthenticationService { * @param scopes The scopes for the session * @param activateImmediate If true, the provider should activate immediately if it is not already */ - getSessions(id: string, scopes?: string[], activateImmediate?: boolean): Promise>; + getSessions(id: string, scopes?: string[], account?: AuthenticationSessionAccount, activateImmediate?: boolean): Promise>; /** * Creates an AuthenticationSession with the given provider and scopes @@ -162,8 +166,12 @@ export interface IAuthenticationExtensionsService { requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise; } -export interface IAuthenticationProviderCreateSessionOptions { - sessionToRecreate?: AuthenticationSession; +export interface IAuthenticationProviderSessionOptions { + /** + * The account that is being asked about. If this is passed in, the provider should + * attempt to return the sessions that are only related to this account. + */ + account?: AuthenticationSessionAccount; } /** @@ -194,9 +202,10 @@ export interface IAuthenticationProvider { /** * Retrieves a list of authentication sessions. * @param scopes - An optional list of scopes. If provided, the sessions returned should match these permissions, otherwise all sessions should be returned. + * @param options - Additional options for getting sessions. * @returns A promise that resolves to an array of authentication sessions. */ - getSessions(scopes?: string[]): Promise; + getSessions(scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions): Promise; /** * Prompts the user to log in. @@ -207,7 +216,7 @@ export interface IAuthenticationProvider { * @param options - Additional options for creating the session. * @returns A promise that resolves to an authentication session. */ - createSession(scopes: string[], options: IAuthenticationProviderCreateSessionOptions): Promise; + createSession(scopes: string[], options: IAuthenticationProviderSessionOptions): Promise; /** * Removes the session corresponding to the specified session ID. diff --git a/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts b/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts index 854aea75cda..891c718ac73 100644 --- a/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts +++ b/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; diff --git a/src/vs/workbench/services/commands/test/common/commandService.test.ts b/src/vs/workbench/services/commands/test/common/commandService.test.ts index 38c6f24d3f4..0c87ce176e1 100644 --- a/src/vs/workbench/services/commands/test/common/commandService.test.ts +++ b/src/vs/workbench/services/commands/test/common/commandService.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { CommandService } from 'vs/workbench/services/commands/common/commandService'; diff --git a/src/vs/workbench/services/configuration/test/browser/configuration.test.ts b/src/vs/workbench/services/configuration/test/browser/configuration.test.ts index b03e14cb2d5..266ccaf978b 100644 --- a/src/vs/workbench/services/configuration/test/browser/configuration.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configuration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts index 31139bf5737..8575a047a79 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as sinon from 'sinon'; -import * as assert from 'assert'; +import assert from 'assert'; import * as json from 'vs/base/common/json'; import { Event } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index eab8d6064f5..bd6c6a3d5ef 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { URI } from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts index 9cfc93c8dfe..bbfd61cdd7d 100644 --- a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts +++ b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Registry } from 'vs/platform/registry/common/platform'; import { StandaloneConfigurationModelParser, Configuration } from 'vs/workbench/services/configuration/common/configurationModels'; import { ConfigurationModelParser, ConfigurationModel, ConfigurationParseOptions } from 'vs/platform/configuration/common/configurationModels'; diff --git a/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts index de7b63c75a0..a7c716da65d 100644 --- a/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts @@ -285,7 +285,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR private showUserInput(variable: string, inputInfos: ConfiguredInput[]): Promise { if (!inputInfos) { - return Promise.reject(new Error(nls.localize('inputVariable.noInputSection', "Variable '{0}' must be defined in an '{1}' section of the debug or task configuration.", variable, 'input'))); + return Promise.reject(new Error(nls.localize('inputVariable.noInputSection', "Variable '{0}' must be defined in an '{1}' section of the debug or task configuration.", variable, 'inputs'))); } // find info for the given input variable diff --git a/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts index b776e512ce8..c3636300a65 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { stub } from 'sinon'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index edf7fb27300..7453c76c816 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -245,8 +245,9 @@ export class DecorationsService implements IDecorationsService { declare _serviceBrand: undefined; - private readonly _onDidChangeDecorationsDelayed = new DebounceEmitter({ merge: all => all.flat() }); - private readonly _onDidChangeDecorations = new Emitter(); + private readonly _store = new DisposableStore(); + private readonly _onDidChangeDecorationsDelayed = this._store.add(new DebounceEmitter({ merge: all => all.flat() })); + private readonly _onDidChangeDecorations = this._store.add(new Emitter()); onDidChangeDecorations: Event = this._onDidChangeDecorations.event; @@ -261,12 +262,11 @@ export class DecorationsService implements IDecorationsService { this._decorationStyles = new DecorationStyles(themeService); this._data = TernarySearchTree.forUris(key => uriIdentityService.extUri.ignorePathCasing(key)); - this._onDidChangeDecorationsDelayed.event(event => { this._onDidChangeDecorations.fire(new FileDecorationChangeEvent(event)); }); + this._store.add(this._onDidChangeDecorationsDelayed.event(event => { this._onDidChangeDecorations.fire(new FileDecorationChangeEvent(event)); })); } dispose(): void { - this._onDidChangeDecorations.dispose(); - this._onDidChangeDecorationsDelayed.dispose(); + this._store.dispose(); this._data.clear(); } diff --git a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts index ae94c92179d..e3e9fbb209e 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DecorationsService } from 'vs/workbench/services/decorations/browser/decorationsService'; import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/decorations/common/decorations'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 5f3b9cbda7d..95be556e81a 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -832,7 +832,7 @@ export class SimpleFileDialog implements ISimpleFileDialog { } else if (!statDirname.isDirectory) { this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateNonexistentDir', 'Please enter a path that exists.'); return Promise.resolve(false); - } else if (statDirname.readonly || statDirname.locked) { + } else if (statDirname.readonly) { this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateReadonlyFolder', 'This folder cannot be used as a save destination. Please choose another folder'); return Promise.resolve(false); } diff --git a/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts b/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts index 5439e7be42f..37db54ee493 100644 --- a/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts +++ b/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; @@ -101,7 +101,7 @@ suite('FileDialogService', function () { const dialogService = instantiationService.createInstance(TestFileDialogService, new TestSimpleFileDialog()); instantiationService.set(IFileDialogService, dialogService); - const workspaceService: IWorkspaceEditingService = instantiationService.createInstance(BrowserWorkspaceEditingService); + const workspaceService: IWorkspaceEditingService = disposables.add(instantiationService.createInstance(BrowserWorkspaceEditingService)); assert.strictEqual((await workspaceService.pickNewWorkspacePath())?.path.startsWith(testFile.path), true); assert.strictEqual(await dialogService.pickWorkspaceAndOpen({}), undefined); }); @@ -126,7 +126,7 @@ suite('FileDialogService', function () { } as IPathService); const dialogService = instantiationService.createInstance(TestFileDialogService, new TestSimpleFileDialog()); instantiationService.set(IFileDialogService, dialogService); - const workspaceService: IWorkspaceEditingService = instantiationService.createInstance(BrowserWorkspaceEditingService); + const workspaceService: IWorkspaceEditingService = disposables.add(instantiationService.createInstance(BrowserWorkspaceEditingService)); assert.strictEqual((await workspaceService.pickNewWorkspacePath())?.path.startsWith(testFile.path), true); assert.strictEqual(await dialogService.pickWorkspaceAndOpen({}), undefined); }); @@ -158,7 +158,7 @@ suite('FileDialogService', function () { } as IPathService); const dialogService = instantiationService.createInstance(TestFileDialogService, new TestSimpleFileDialog()); instantiationService.set(IFileDialogService, dialogService); - const workspaceService: IWorkspaceEditingService = instantiationService.createInstance(BrowserWorkspaceEditingService); + const workspaceService: IWorkspaceEditingService = disposables.add(instantiationService.createInstance(BrowserWorkspaceEditingService)); assert.strictEqual((await workspaceService.pickNewWorkspacePath())?.path.startsWith(testFile.path), true); assert.strictEqual(await dialogService.pickWorkspaceAndOpen({}), undefined); }); diff --git a/src/vs/workbench/services/editor/common/customEditorLabelService.ts b/src/vs/workbench/services/editor/common/customEditorLabelService.ts index 8264adcbd91..bd6633067a1 100644 --- a/src/vs/workbench/services/editor/common/customEditorLabelService.ts +++ b/src/vs/workbench/services/editor/common/customEditorLabelService.ts @@ -51,10 +51,10 @@ export class CustomEditorLabelService extends Disposable implements ICustomEdito this.storeEnablementState(); this.storeCustomPatterns(); - this.registerListernes(); + this.registerListeners(); } - private registerListernes(): void { + private registerListeners(): void { this._register(this.configurationService.onDidChangeConfiguration(e => { // Cache the enabled state if (e.affectsConfiguration(CustomEditorLabelService.SETTING_ID_ENABLED)) { @@ -148,29 +148,43 @@ export class CustomEditorLabelService extends Disposable implements ICustomEdito } if (pattern.parsedPattern(relevantPath)) { - return this.applyTempate(pattern.template, resource, relevantPath); + return this.applyTemplate(pattern.template, resource, relevantPath); } } return undefined; } - private readonly _parsedTemplateExpression = /\$\{(dirname|filename|extname|dirname\(([-+]?\d+)\))\}/g; - private applyTempate(template: string, resource: URI, relevantPath: string): string { + private readonly _parsedTemplateExpression = /\$\{(dirname|filename|extname|extname\((?[-+]?\d+)\)|dirname\((?[-+]?\d+)\))\}/g; + private readonly _filenameCaptureExpression = /(?^\.*[^.]*)/; + private applyTemplate(template: string, resource: URI, relevantPath: string): string { let parsedPath: undefined | ParsedPath; - return template.replace(this._parsedTemplateExpression, (match: string, variable: string, arg: string) => { + return template.replace(this._parsedTemplateExpression, (match: string, variable: string, ...args: any[]) => { parsedPath = parsedPath ?? parsePath(resource.path); - switch (variable) { - case 'filename': - return parsedPath.name; - case 'extname': - return parsedPath.ext.slice(1); - default: { // dirname and dirname(arg) - const n = variable === 'dirname' ? 0 : parseInt(arg); - const nthDir = this.getNthDirname(dirname(relevantPath), n); - if (nthDir) { - return nthDir; - } + // named group matches + const { dirnameN = '0', extnameN = '0' }: { dirnameN?: string; extnameN?: string } = args.pop(); + + if (variable === 'filename') { + const { filename } = this._filenameCaptureExpression.exec(parsedPath.base)?.groups ?? {}; + if (filename) { + return filename; + } + } else if (variable === 'extname') { + const extension = this.getExtnames(parsedPath.base); + if (extension) { + return extension; + } + } else if (variable.startsWith('extname')) { + const n = parseInt(extnameN); + const nthExtname = this.getNthExtname(parsedPath.base, n); + if (nthExtname) { + return nthExtname; + } + } else if (variable.startsWith('dirname')) { + const n = parseInt(dirnameN); + const nthDir = this.getNthDirname(dirname(relevantPath), n); + if (nthDir) { + return nthDir; } } @@ -178,12 +192,36 @@ export class CustomEditorLabelService extends Disposable implements ICustomEdito }); } + private removeLeadingDot(path: string): string { + let withoutLeadingDot = path; + while (withoutLeadingDot.startsWith('.')) { + withoutLeadingDot = withoutLeadingDot.slice(1); + } + return withoutLeadingDot; + } + private getNthDirname(path: string, n: number): string | undefined { // grand-parent/parent/filename.ext1.ext2 -> [grand-parent, parent] path = path.startsWith('/') ? path.slice(1) : path; const pathFragments = path.split('/'); - const length = pathFragments.length; + return this.getNthFragment(pathFragments, n); + } + + private getExtnames(fullFileName: string): string { + return this.removeLeadingDot(fullFileName).split('.').slice(1).join('.'); + } + + private getNthExtname(fullFileName: string, n: number): string | undefined { + // file.ext1.ext2.ext3 -> [file, ext1, ext2, ext3] + const extensionNameFragments = this.removeLeadingDot(fullFileName).split('.'); + extensionNameFragments.shift(); // remove the first element which is the file name + + return this.getNthFragment(extensionNameFragments, n); + } + + private getNthFragment(fragments: string[], n: number): string | undefined { + const length = fragments.length; let nth; if (n < 0) { @@ -192,11 +230,11 @@ export class CustomEditorLabelService extends Disposable implements ICustomEdito nth = length - n - 1; } - const nthDir = pathFragments[nth]; - if (nthDir === undefined || nthDir === '') { + const nthFragment = fragments[nth]; + if (nthFragment === undefined || nthFragment === '') { return undefined; } - return nthDir; + return nthFragment; } } diff --git a/src/vs/workbench/services/editor/test/browser/customEditorLabelService.test.ts b/src/vs/workbench/services/editor/test/browser/customEditorLabelService.test.ts new file mode 100644 index 00000000000..343c70585d7 --- /dev/null +++ b/src/vs/workbench/services/editor/test/browser/customEditorLabelService.test.ts @@ -0,0 +1,234 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { CustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService'; +import { ITestInstantiationService, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; + +suite('Custom Editor Label Service', () => { + + const disposables = new DisposableStore(); + + setup(() => { }); + + teardown(async () => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + async function createCustomLabelService(instantiationService: ITestInstantiationService = workbenchInstantiationService(undefined, disposables)): Promise<[CustomEditorLabelService, TestConfigurationService, TestServiceAccessor]> { + const configService = new TestConfigurationService(); + await configService.setUserConfiguration(CustomEditorLabelService.SETTING_ID_ENABLED, true); + instantiationService.stub(IConfigurationService, configService); + + const customLabelService = disposables.add(instantiationService.createInstance(CustomEditorLabelService)); + return [customLabelService, configService, instantiationService.createInstance(TestServiceAccessor)]; + } + + async function updatePattern(configService: TestConfigurationService, value: any): Promise { + await configService.setUserConfiguration(CustomEditorLabelService.SETTING_ID_PATTERNS, value); + configService.onDidChangeConfigurationEmitter.fire({ + affectsConfiguration: (key: string) => key === CustomEditorLabelService.SETTING_ID_PATTERNS, + source: ConfigurationTarget.USER, + affectedKeys: new Set(CustomEditorLabelService.SETTING_ID_PATTERNS), + change: { + keys: [], + overrides: [] + } + }); + } + + test('Custom Labels: filename.extname', async () => { + const [customLabelService, configService] = await createCustomLabelService(); + + await updatePattern(configService, { + '**': '${filename}.${extname}' + }); + + const filenames = [ + 'file.txt', + 'file.txt1.tx2', + '.file.txt', + ]; + + for (const filename of filenames) { + const label = customLabelService.getName(URI.file(filename)); + assert.strictEqual(label, filename); + } + + let label = customLabelService.getName(URI.file('file')); + assert.strictEqual(label, 'file.${extname}'); + + label = customLabelService.getName(URI.file('.file')); + assert.strictEqual(label, '.file.${extname}'); + }); + + test('Custom Labels: filename', async () => { + const [customLabelService, configService] = await createCustomLabelService(); + + await updatePattern(configService, { + '**': '${filename}', + }); + + assert.strictEqual(customLabelService.getName(URI.file('file')), 'file'); + assert.strictEqual(customLabelService.getName(URI.file('file.txt')), 'file'); + assert.strictEqual(customLabelService.getName(URI.file('file.txt1.txt2')), 'file'); + assert.strictEqual(customLabelService.getName(URI.file('folder/file.txt1.txt2')), 'file'); + + assert.strictEqual(customLabelService.getName(URI.file('.file')), '.file'); + assert.strictEqual(customLabelService.getName(URI.file('.file.txt')), '.file'); + assert.strictEqual(customLabelService.getName(URI.file('.file.txt1.txt2')), '.file'); + assert.strictEqual(customLabelService.getName(URI.file('folder/.file.txt1.txt2')), '.file'); + }); + + test('Custom Labels: extname(N)', async () => { + const [customLabelService, configService] = await createCustomLabelService(); + + await updatePattern(configService, { + '**/ext/**': '${extname}', + '**/ext0/**': '${extname(0)}', + '**/ext1/**': '${extname(1)}', + '**/ext2/**': '${extname(2)}', + '**/extMinus1/**': '${extname(-1)}', + '**/extMinus2/**': '${extname(-2)}', + }); + + interface IExt { + extname?: string; + ext0?: string; + ext1?: string; + ext2?: string; + extMinus1?: string; + extMinus2?: string; + } + + function assertExtname(filename: string, ext: IExt): void { + assert.strictEqual(customLabelService.getName(URI.file(`test/ext/${filename}`)), ext.extname ?? '${extname}', filename); + assert.strictEqual(customLabelService.getName(URI.file(`test/ext0/${filename}`)), ext.ext0 ?? '${extname(0)}', filename); + assert.strictEqual(customLabelService.getName(URI.file(`test/ext1/${filename}`)), ext.ext1 ?? '${extname(1)}', filename); + assert.strictEqual(customLabelService.getName(URI.file(`test/ext2/${filename}`)), ext.ext2 ?? '${extname(2)}', filename); + assert.strictEqual(customLabelService.getName(URI.file(`test/extMinus1/${filename}`)), ext.extMinus1 ?? '${extname(-1)}', filename); + assert.strictEqual(customLabelService.getName(URI.file(`test/extMinus2/${filename}`)), ext.extMinus2 ?? '${extname(-2)}', filename); + } + + assertExtname('file.txt', { + extname: 'txt', + ext0: 'txt', + extMinus1: 'txt', + }); + + assertExtname('file.txt1.txt2', { + extname: 'txt1.txt2', + ext0: 'txt2', + ext1: 'txt1', + extMinus1: 'txt1', + extMinus2: 'txt2', + }); + + assertExtname('.file.txt1.txt2', { + extname: 'txt1.txt2', + ext0: 'txt2', + ext1: 'txt1', + extMinus1: 'txt1', + extMinus2: 'txt2', + }); + + assertExtname('.file.txt1.txt2.txt3.txt4', { + extname: 'txt1.txt2.txt3.txt4', + ext0: 'txt4', + ext1: 'txt3', + ext2: 'txt2', + extMinus1: 'txt1', + extMinus2: 'txt2', + }); + + assertExtname('file', {}); + assertExtname('.file', {}); + }); + + test('Custom Labels: dirname(N)', async () => { + const [customLabelService, configService] = await createCustomLabelService(); + + await updatePattern(configService, { + '**': '${dirname},${dirname(0)},${dirname(1)},${dirname(2)},${dirname(-1)},${dirname(-2)}', + }); + + interface IDir { + dirname?: string; + dir0?: string; + dir1?: string; + dir2?: string; + dirMinus1?: string; + dirMinus2?: string; + } + + function assertDirname(path: string, dir: IDir): void { + assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[0], dir.dirname ?? '${dirname}', path); + assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[1], dir.dir0 ?? '${dirname(0)}', path); + assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[2], dir.dir1 ?? '${dirname(1)}', path); + assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[3], dir.dir2 ?? '${dirname(2)}', path); + assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[4], dir.dirMinus1 ?? '${dirname(-1)}', path); + assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[5], dir.dirMinus2 ?? '${dirname(-2)}', path); + } + + assertDirname('folder/file.txt', { + dirname: 'folder', + dir0: 'folder', + dirMinus1: 'folder', + }); + + assertDirname('root/folder/file.txt', { + dirname: 'folder', + dir0: 'folder', + dir1: 'root', + dirMinus1: 'root', + dirMinus2: 'folder', + }); + + assertDirname('root/.folder/file.txt', { + dirname: '.folder', + dir0: '.folder', + dir1: 'root', + dirMinus1: 'root', + dirMinus2: '.folder', + }); + + assertDirname('root/parent/folder/file.txt', { + dirname: 'folder', + dir0: 'folder', + dir1: 'parent', + dir2: 'root', + dirMinus1: 'root', + dirMinus2: 'parent', + }); + + assertDirname('file.txt', {}); + }); + + test('Custom Labels: no pattern match', async () => { + const [customLabelService, configService] = await createCustomLabelService(); + + await updatePattern(configService, { + '**/folder/**': 'folder', + 'file': 'file', + }); + + assert.strictEqual(customLabelService.getName(URI.file('file')), undefined); + assert.strictEqual(customLabelService.getName(URI.file('file.txt')), undefined); + assert.strictEqual(customLabelService.getName(URI.file('file.txt1.txt2')), undefined); + assert.strictEqual(customLabelService.getName(URI.file('folder1/file.txt1.txt2')), undefined); + + assert.strictEqual(customLabelService.getName(URI.file('.file')), undefined); + assert.strictEqual(customLabelService.getName(URI.file('.file.txt')), undefined); + assert.strictEqual(customLabelService.getName(URI.file('.file.txt1.txt2')), undefined); + assert.strictEqual(customLabelService.getName(URI.file('folder1/file.txt1.txt2')), undefined); + }); +}); diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 80455d79ac1..e2fd3321cac 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, TestServiceAccessor, ITestInstantiationService, workbenchTeardown, createEditorParts, TestEditorParts } from 'vs/workbench/test/browser/workbenchTestServices'; import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService, GroupsArrangement, IEditorGroupContextKeyProvider } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CloseDirection, IEditorPartOptions, EditorsOrder, EditorInputCapabilities, GroupModelChangeKind, SideBySideEditor, IEditorFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; diff --git a/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts b/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts index 2e44db0f38c..59ddcd67c57 100644 --- a/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 7671e480d79..ce6d1a08d6c 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EditorActivation, IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; diff --git a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts index 4d8100c5640..b968288e9d2 100644 --- a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IEditorFactoryRegistry, EditorExtensions, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor, TestEditorPart, createEditorPart, registerTestSideBySideEditor } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index 2432b8ac780..9761cc48279 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBuiltinExtensionsScannerService, ExtensionType, IExtensionIdentifier, IExtension, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { IBuiltinExtensionsScannerService, ExtensionType, IExtensionIdentifier, IExtension, IExtensionManifest, TargetPlatform, IRelaxedExtensionManifest, parseEnabledApiProposalNames } from 'vs/platform/extensions/common/extensions'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IScannedExtension, IWebExtensionsScannerService, ScanOptions } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { isWeb, Language } from 'vs/base/common/platform'; @@ -103,6 +103,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten private readonly systemExtensionsCacheResource: URI | undefined = undefined; private readonly customBuiltinExtensionsCacheResource: URI | undefined = undefined; private readonly resourcesAccessQueueMap = new ResourceMap>(); + private readonly extensionsEnabledWithApiProposalVersion: string[]; constructor( @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, @@ -127,6 +128,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten // Eventually update caches lifecycleService.when(LifecyclePhase.Eventually).then(() => this.updateCaches()); } + this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? []; } private _customBuiltinExtensionsInfoPromise: Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[]; extensionGalleryResources: URI[] }> | undefined; @@ -739,7 +741,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten private async toScannedExtension(webExtension: IWebExtension, isBuiltin: boolean, type: ExtensionType = ExtensionType.User): Promise { const validations: [Severity, string][] = []; - let manifest: IExtensionManifest | undefined = webExtension.manifest; + let manifest: IRelaxedExtensionManifest | undefined = webExtension.manifest; if (!manifest) { try { @@ -770,7 +772,8 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const uuid = (webExtension.metadata)?.id; - validations.push(...validateExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false)); + const validateApiVersion = this.extensionsEnabledWithApiProposalVersion.includes(webExtension.identifier.id.toLowerCase()); + validations.push(...validateExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false, validateApiVersion)); // --- Start Positron --- validations.push(...validatePositronExtensionManifest(this.productService.positronVersion, this.productService.date, webExtension.location, manifest, false)); // --- End Positron --- @@ -782,6 +785,10 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } } + if (manifest.enabledApiProposals && validateApiVersion) { + manifest.enabledApiProposals = parseEnabledApiProposalNames([...manifest.enabledApiProposals]); + } + return { identifier: { id: webExtension.identifier.id, uuid: webExtension.identifier.uuid || uuid }, location: webExtension.location, @@ -807,7 +814,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return []; } - private async translateManifest(manifest: IExtensionManifest, nlsURL: ITranslations | URI, fallbackNLS?: ITranslations | URI): Promise { + private async translateManifest(manifest: IExtensionManifest, nlsURL: ITranslations | URI, fallbackNLS?: ITranslations | URI): Promise { try { const translations = URI.isUri(nlsURL) ? await this.getTranslations(nlsURL) : nlsURL; const fallbackTranslations = URI.isUri(fallbackNLS) ? await this.getTranslations(fallbackNLS) : fallbackNLS; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 3c7751e1f6b..4886c6ff699 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -78,6 +78,7 @@ export interface IWorkbenchExtensionManagementService extends IProfileAwareExten installResourceExtension(extension: IResourceExtension, installOptions: InstallOptions): Promise; updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension, installOptions?: InstallOptions): Promise; + updateMetadata(local: ILocalExtension, metadata: Partial): Promise; } export const extensionsConfigurationNodeBase = { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts index 8c86a741081..dc87c77cb21 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, Metadata, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallExtensionInfo, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, Metadata, InstallExtensionResult, InstallExtensionInfo, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier, ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ExtensionManagementChannelClient as BaseExtensionManagementChannelClient, ExtensionEventResult } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; @@ -32,19 +32,15 @@ export abstract class ProfileAwareExtensionManagementChannelClient extends BaseE })); } - protected override fireEvent(event: Emitter, data: InstallExtensionEvent): Promise; - protected override fireEvent(event: Emitter, data: InstallExtensionResult[]): Promise; - protected override fireEvent(event: Emitter, data: UninstallExtensionEvent): Promise; - protected override fireEvent(event: Emitter, data: DidUninstallExtensionEvent): Promise; - protected override fireEvent(event: Emitter, data: ExtensionEventResult): Promise; - protected override fireEvent(event: Emitter, data: ExtensionEventResult[]): Promise; + protected override async fireEvent(event: Emitter, data: E): Promise; + protected override async fireEvent(event: Emitter, data: E[]): Promise; protected override async fireEvent(arg0: any, arg1: any): Promise { if (Array.isArray(arg1)) { const event = arg0 as Emitter; const data = arg1 as ExtensionEventResult[]; const filtered = []; for (const e of data) { - const result = this.filterEvent(e); + const result = this.filterEvent(e.profileLocation, e.applicationScoped ?? e.local?.isApplicationScoped ?? false); if (result instanceof Promise ? await result : result) { filtered.push(e); } @@ -55,7 +51,7 @@ export abstract class ProfileAwareExtensionManagementChannelClient extends BaseE } else { const event = arg0 as Emitter; const data = arg1 as ExtensionEventResult; - const result = this.filterEvent(data); + const result = this.filterEvent(data.profileLocation, data.applicationScoped ?? data.local?.isApplicationScoped ?? false); if (result instanceof Promise ? await result : result) { event.fire(data); } @@ -141,5 +137,5 @@ export abstract class ProfileAwareExtensionManagementChannelClient extends BaseE return profileLocation ?? this.userDataProfileService.currentProfile.extensionsResource; } - protected abstract filterEvent(e: ExtensionEventResult): boolean | Promise; + protected abstract filterEvent(profileLocation: URI, isApplicationScoped: boolean): boolean | Promise; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index be81abb2d40..7dc4aa2abc7 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -7,7 +7,8 @@ import { Emitter, Event, EventMultiplexer } from 'vs/base/common/event'; import { ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SOURCE_CONTEXT, InstallExtensionInfo, IProductVersion, - ExtensionInstallSource + ExtensionInstallSource, + DidUpdateExtensionMetadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IResourceExtension, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -60,7 +61,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench private readonly _onDidUninstallExtension = this._register(new Emitter()); readonly onDidUninstallExtension: Event; - readonly onDidUpdateExtensionMetadata: Event; + readonly onDidUpdateExtensionMetadata: Event; readonly onDidChangeProfile: Event; readonly onDidEnableExtensions: Event; @@ -117,7 +118,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench this._register(onDidUninstallExtensionEventMultiplexer.add(this._onDidUninstallExtension.event)); this.onDidUninstallExtension = onDidUninstallExtensionEventMultiplexer.event; - const onDidUpdateExtensionMetadaEventMultiplexer = this._register(new EventMultiplexer()); + const onDidUpdateExtensionMetadaEventMultiplexer = this._register(new EventMultiplexer()); this.onDidUpdateExtensionMetadata = onDidUpdateExtensionMetadaEventMultiplexer.event; const onDidChangeProfileEventMultiplexer = this._register(new EventMultiplexer()); @@ -217,10 +218,10 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return Promise.reject(`Invalid location ${extension.location.toString()}`); } - updateMetadata(extension: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise { + updateMetadata(extension: ILocalExtension, metadata: Partial): Promise { const server = this.getServer(extension); if (server) { - return server.extensionManagementService.updateMetadata(extension, metadata, profileLocation); + return server.extensionManagementService.updateMetadata(extension, metadata, this.userDataProfileService.currentProfile.extensionsResource); } return Promise.reject(`Invalid location ${extension.location.toString()}`); } @@ -383,6 +384,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench servers.push(server); } + installOptions = { ...(installOptions || {}), isApplicationScoped: extension.isApplicationScoped }; return Promises.settled(servers.map(server => server.extensionManagementService.installFromGallery(gallery, installOptions))).then(([local]) => local); } @@ -406,7 +408,12 @@ export class ExtensionManagementService extends Disposable implements IWorkbench exensions.push({ extension, options }); } } catch (error) { - results.set(extension.identifier.id.toLowerCase(), { identifier: extension.identifier, source: extension, error, operation: InstallOperation.Install }); + results.set(extension.identifier.id.toLowerCase(), { + identifier: extension.identifier, + source: extension, error, + operation: InstallOperation.Install, + profileLocation: options.profileLocation ?? this.userDataProfileService.currentProfile.extensionsResource + }); } })); @@ -530,7 +537,8 @@ export class ExtensionManagementService extends Disposable implements IWorkbench identifier: extension.identifier, server, applicationScoped: false, - workspaceScoped: true + workspaceScoped: true, + profileLocation: this.userDataProfileService.currentProfile.extensionsResource }); try { @@ -544,7 +552,8 @@ export class ExtensionManagementService extends Disposable implements IWorkbench identifier: extension.identifier, server, applicationScoped: false, - workspaceScoped: true + workspaceScoped: true, + profileLocation: this.userDataProfileService.currentProfile.extensionsResource }); } catch (error) { this.logService.error(`Failed to uninstall the workspace extension ${extension.identifier.id} from ${extension.location.toString()}`, getErrorMessage(error)); @@ -553,7 +562,8 @@ export class ExtensionManagementService extends Disposable implements IWorkbench server, error, applicationScoped: false, - workspaceScoped: true + workspaceScoped: true, + profileLocation: this.userDataProfileService.currentProfile.extensionsResource }); throw error; } diff --git a/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts index 0e2c59c618d..17643ad3a5e 100644 --- a/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts @@ -11,7 +11,6 @@ import { IRemoteUserDataProfilesService } from 'vs/workbench/services/userDataPr import { ProfileAwareExtensionManagementChannelClient } from 'vs/workbench/services/extensionManagement/common/extensionManagementChannelClient'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { ExtensionEventResult } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export class RemoteExtensionManagementService extends ProfileAwareExtensionManagementChannelClient implements IProfileAwareExtensionManagementService { @@ -26,15 +25,15 @@ export class RemoteExtensionManagementService extends ProfileAwareExtensionManag super(channel, userDataProfileService, uriIdentityService); } - protected async filterEvent(e: ExtensionEventResult): Promise { - if (e.applicationScoped) { + protected async filterEvent(profileLocation: URI, applicationScoped: boolean): Promise { + if (applicationScoped) { return true; } - if (!e.profileLocation && this.userDataProfileService.currentProfile.isDefault) { + if (!profileLocation && this.userDataProfileService.currentProfile.isDefault) { return true; } const currentRemoteProfile = await this.remoteUserDataProfilesService.getRemoteProfile(this.userDataProfileService.currentProfile); - if (this.uriIdentityService.extUri.isEqual(currentRemoteProfile.extensionsResource, e.profileLocation)) { + if (this.uriIdentityService.extUri.isEqual(currentRemoteProfile.extensionsResource, profileLocation)) { return true; } return false; diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index e925b29bc64..a68f7115fb6 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -149,7 +149,7 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe return result; } - async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise { + async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI): Promise { // unset if false if (metadata.isMachineScoped === false) { metadata.isMachineScoped = undefined; @@ -160,9 +160,9 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe if (metadata.pinned === false) { metadata.pinned = undefined; } - const updatedExtension = await this.webExtensionsScannerService.updateMetadata(local, metadata, profileLocation ?? this.userDataProfileService.currentProfile.extensionsResource); + const updatedExtension = await this.webExtensionsScannerService.updateMetadata(local, metadata, profileLocation); const updatedLocalExtension = toLocalExtension(updatedExtension); - this._onDidUpdateExtensionMetadata.fire(updatedLocalExtension); + this._onDidUpdateExtensionMetadata.fire({ local: updatedLocalExtension, profileLocation }); return updatedLocalExtension; } diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts index 6953afe5736..f5a980a319a 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts @@ -33,8 +33,8 @@ export class NativeExtensionManagementService extends ProfileAwareExtensionManag super(channel, userDataProfileService, uriIdentityService); } - protected filterEvent({ profileLocation, applicationScoped }: { readonly profileLocation?: URI; readonly applicationScoped?: boolean }): boolean { - return applicationScoped || this.uriIdentityService.extUri.isEqual(this.userDataProfileService.currentProfile.extensionsResource, profileLocation); + protected filterEvent(profileLocation: URI, isApplicationScoped: boolean): boolean { + return isApplicationScoped || this.uriIdentityService.extUri.isEqual(this.userDataProfileService.currentProfile.extensionsResource, profileLocation); } override async install(vsix: URI, options?: InstallOptions): Promise { diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index 79cf8b0ad4e..3ca587e7c8b 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -24,6 +24,7 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IRemoteUserDataProfilesService } from 'vs/workbench/services/userDataProfile/common/remoteUserDataProfiles'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { areApiProposalsCompatible } from 'vs/platform/extensions/common/extensionValidator'; export class NativeRemoteExtensionManagementService extends RemoteExtensionManagementService { @@ -134,6 +135,10 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag } if (!compatibleExtension) { + const incompatibleApiProposalsMessages: string[] = []; + if (!areApiProposalsCompatible(extension.properties.enabledApiProposals ?? [], incompatibleApiProposalsMessages)) { + throw new ExtensionManagementError(localize('incompatibleAPI', "Can't install '{0}' extension. {1}", extension.displayName ?? extension.identifier.id, incompatibleApiProposalsMessages[0]), ExtensionManagementErrorCode.IncompatibleApi); + } /** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */ if (!includePreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) { throw new ExtensionManagementError(localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound); diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 4794c1ee757..89838969713 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; -import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, DidUpdateExtensionMetadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, ExtensionInstallLocation, IProfileAwareExtensionManagementService, DidChangeProfileEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionEnablementService } from 'vs/workbench/services/extensionManagement/browser/extensionEnablementService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; @@ -70,7 +70,7 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { onUninstallExtension: disposables.add(new Emitter()).event, onDidUninstallExtension: disposables.add(new Emitter()).event, onDidChangeProfile: disposables.add(new Emitter()).event, - onDidUpdateExtensionMetadata: disposables.add(new Emitter()).event, + onDidUpdateExtensionMetadata: disposables.add(new Emitter()).event, }, }, null, null)); const extensionManagementService = disposables.add(instantiationService.createInstance(ExtensionManagementService)); @@ -456,7 +456,7 @@ suite('ExtensionEnablementService Test', () => { await testObject.setEnablement([extension], EnablementState.DisabledWorkspace); await testObject.setEnablement([extension], EnablementState.DisabledGlobally); - didUninstallEvent.fire({ identifier: { id: 'pub.a' } }); + didUninstallEvent.fire({ identifier: { id: 'pub.a' }, profileLocation: null! }); assert.ok(testObject.isEnabled(extension)); assert.strictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 1611fb02870..a5596ccb66c 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -8,7 +8,7 @@ import { Schemas } from 'vs/base/common/network'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ExtensionKind } from 'vs/platform/environment/common/environment'; -import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -111,19 +111,6 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._initFetchFileSystem(); } - protected async _scanSingleExtension(extension: IExtension): Promise { - if (extension.location.scheme === Schemas.vscodeRemote) { - return this._remoteExtensionsScannerService.scanSingleExtension(extension.location, extension.type === ExtensionType.System); - } - - const scannedExtension = await this._webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, this._userDataProfileService.currentProfile.extensionsResource); - if (scannedExtension) { - return toExtensionDescription(scannedExtension); - } - - return null; - } - private _initFetchFileSystem(): void { const provider = new FetchFileSystemProvider(); this._register(this._fileService.registerProvider(Schemas.http, provider)); diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 5faa4fb7cac..79d9ecde2e3 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -7,7 +7,7 @@ import { Barrier } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as perf from 'vs/base/common/performance'; import { isCI } from 'vs/base/common/platform'; @@ -45,7 +45,7 @@ import { IResolveAuthorityErrorResult } from 'vs/workbench/services/extensions/c import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { ExtensionRunningLocation, LocalProcessRunningLocation, LocalWebWorkerRunningLocation, RemoteRunningLocation } from 'vs/workbench/services/extensions/common/extensionRunningLocation'; import { ExtensionRunningLocationTracker, filterExtensionIdentifiers } from 'vs/workbench/services/extensions/common/extensionRunningLocationTracker'; -import { ActivationKind, ActivationTimes, ExtensionActivationReason, ExtensionHostStartup, ExtensionPointContribution, IExtensionHost, IExtensionService, IExtensionsStatus, IInternalExtensionService, IMessage, IResponsiveStateChangeEvent, IWillActivateEvent, WillStopExtensionHostsEvent, toExtension } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationKind, ActivationTimes, ExtensionActivationReason, ExtensionHostStartup, ExtensionPointContribution, IExtensionHost, IExtensionService, IExtensionsStatus, IInternalExtensionService, IMessage, IResponsiveStateChangeEvent, IWillActivateEvent, WillStopExtensionHostsEvent, toExtension, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionsProposedApi } from 'vs/workbench/services/extensions/common/extensionsProposedApi'; import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { LazyCreateExtensionHostManager } from 'vs/workbench/services/extensions/common/lazyCreateExtensionHostManager'; @@ -97,7 +97,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx private _deltaExtensionsQueue: DeltaExtensionsQueueItem[] = []; private _inHandleDeltaExtensions = false; - private _extensionHostManagers: IExtensionHostManager[] = []; + private readonly _extensionHostManagers = this._register(new ExtensionHostCollection()); private _resolveAuthorityAttempt: number = 0; @@ -214,16 +214,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } protected _getExtensionHostManagers(kind: ExtensionHostKind): IExtensionHostManager[] { - return this._extensionHostManagers.filter(extHostManager => extHostManager.kind === kind); - } - - private _getExtensionHostManagerByRunningLocation(runningLocation: ExtensionRunningLocation): IExtensionHostManager | null { - for (const extensionHostManager of this._extensionHostManagers) { - if (extensionHostManager.representsRunningLocation(runningLocation)) { - return extensionHostManager; - } - } - return null; + return this._extensionHostManagers.getByKind(kind); } //#region deltaExtensions @@ -285,7 +276,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx for (let i = 0, len = _toAdd.length; i < len; i++) { const extension = _toAdd[i]; - const extensionDescription = await this._scanSingleExtension(extension); + const extensionDescription = toExtensionDescription(extension, false); if (!extensionDescription) { // could not scan extension... continue; @@ -582,7 +573,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } } if (runningLocation !== null) { - return this._getExtensionHostManagerByRunningLocation(runningLocation); + return this._extensionHostManagers.getByRunningLocation(runningLocation); } return null; } @@ -687,8 +678,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx //#region Stopping / Starting / Restarting - public stopExtensionHosts(reason: string): Promise { - return this._doStopExtensionHostsWithVeto(reason); + public stopExtensionHosts(reason: string, auto?: boolean): Promise { + return this._doStopExtensionHostsWithVeto(reason, auto); } protected _doStopExtensionHosts(): void { @@ -699,13 +690,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } } - // See https://github.com/microsoft/vscode/issues/152204 - // Dispose extension hosts in reverse creation order because the local extension host - // might be critical in sustaining a connection to the remote extension host - for (let i = this._extensionHostManagers.length - 1; i >= 0; i--) { - this._extensionHostManagers[i].dispose(); - } - this._extensionHostManagers = []; + this._extensionHostManagers.disposeAllInReverse(); for (const extensionStatus of this._extensionStatus.values()) { extensionStatus.clearRuntimeStatus(); } @@ -715,7 +700,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } } - private async _doStopExtensionHostsWithVeto(reason: string): Promise { + private async _doStopExtensionHostsWithVeto(reason: string, auto?: boolean): Promise { const vetos: (boolean | Promise)[] = []; const vetoReasons = new Set(); @@ -744,16 +729,18 @@ export abstract class AbstractExtensionService extends Disposable implements IEx if (!veto) { this._doStopExtensionHosts(); } else { - const vetoReasonsArray = Array.from(vetoReasons); - - this._logService.warn(`Extension host was not stopped because of veto (stop reason: ${reason}, veto reason: ${vetoReasonsArray.join(', ')})`); + if (!auto) { + const vetoReasonsArray = Array.from(vetoReasons); + + this._logService.warn(`Extension host was not stopped because of veto (stop reason: ${reason}, veto reason: ${vetoReasonsArray.join(', ')})`); + await this._dialogService.warn( + nls.localize('extensionStopVetoMessage', "The following operation was blocked: {0}", reason), + vetoReasonsArray.length === 1 ? + nls.localize('extensionStopVetoDetailsOne', "The reason for blocking the operation: {0}", vetoReasonsArray[0]) : + nls.localize('extensionStopVetoDetailsMany', "The reasons for blocking the operation:\n- {0}", vetoReasonsArray.join('\n -')), + ); + } - await this._dialogService.warn( - nls.localize('extensionStopVetoMessage', "The following operation was blocked: {0}", reason), - vetoReasonsArray.length === 1 ? - nls.localize('extensionStopVetoDetailsOne', "The reason for blocking the operation: {0}", vetoReasonsArray[0]) : - nls.localize('extensionStopVetoDetailsMany', "The reasons for blocking the operation:\n- {0}", vetoReasonsArray.join('\n -')), - ); } return !veto; @@ -769,26 +756,28 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } locations.push(new RemoteRunningLocation()); for (const location of locations) { - if (this._getExtensionHostManagerByRunningLocation(location)) { + if (this._extensionHostManagers.getByRunningLocation(location)) { // already running continue; } - const extHostManager = this._createExtensionHostManager(location, isInitialStart, initialActivationEvents); - if (extHostManager) { - this._extensionHostManagers.push(extHostManager); + const res = this._createExtensionHostManager(location, isInitialStart, initialActivationEvents); + if (res) { + const [extHostManager, disposableStore] = res; + this._extensionHostManagers.add(extHostManager, disposableStore); } } } - private _createExtensionHostManager(runningLocation: ExtensionRunningLocation, isInitialStart: boolean, initialActivationEvents: string[]): IExtensionHostManager | null { + private _createExtensionHostManager(runningLocation: ExtensionRunningLocation, isInitialStart: boolean, initialActivationEvents: string[]): null | [IExtensionHostManager, DisposableStore] { const extensionHost = this._extensionHostFactory.createExtensionHost(this._runningLocations, runningLocation, isInitialStart); if (!extensionHost) { return null; } const processManager: IExtensionHostManager = this._doCreateExtensionHostManager(extensionHost, initialActivationEvents); - processManager.onDidExit(([code, signal]) => this._onExtensionHostCrashOrExit(processManager, code, signal)); - processManager.onDidChangeResponsiveState((responsiveState) => { + const disposableStore = new DisposableStore(); + disposableStore.add(processManager.onDidExit(([code, signal]) => this._onExtensionHostCrashOrExit(processManager, code, signal))); + disposableStore.add(processManager.onDidChangeResponsiveState((responsiveState) => { this._logService.info(`Extension host (${processManager.friendyName}) is ${responsiveState === ResponsiveState.Responsive ? 'responsive' : 'unresponsive'}.`); this._onDidChangeResponsiveChange.fire({ extensionHostKind: processManager.kind, @@ -797,8 +786,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return processManager.getInspectPort(tryEnableInspector); } }); - }); - return processManager; + })); + return [processManager, disposableStore]; } protected _doCreateExtensionHostManager(extensionHost: IExtensionHost, initialActivationEvents: string[]): IExtensionHostManager { @@ -829,13 +818,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx if (signal) { this._onRemoteExtensionHostCrashed(extensionHost, signal); } - for (let i = 0; i < this._extensionHostManagers.length; i++) { - if (this._extensionHostManagers[i] === extensionHost) { - this._extensionHostManagers[i].dispose(); - this._extensionHostManagers.splice(i, 1); - break; - } - } + this._extensionHostManagers.disposeOne(extensionHost); } } @@ -1226,11 +1209,85 @@ export abstract class AbstractExtensionService extends Disposable implements IEx //#endregion protected abstract _resolveExtensions(): Promise; - protected abstract _scanSingleExtension(extension: IExtension): Promise; protected abstract _onExtensionHostExit(code: number): void; protected abstract _resolveAuthority(remoteAuthority: string): Promise; } +class ExtensionHostCollection extends Disposable { + + private _extensionHostManagers: ExtensionHostManagerData[] = []; + + public override dispose(): void { + this.disposeAllInReverse(); + super.dispose(); + } + + public add(extensionHostManager: IExtensionHostManager, disposableStore: DisposableStore): void { + this._extensionHostManagers.push(new ExtensionHostManagerData(extensionHostManager, disposableStore)); + } + + public disposeAllInReverse(): void { + // See https://github.com/microsoft/vscode/issues/152204 + // Dispose extension hosts in reverse creation order because the local extension host + // might be critical in sustaining a connection to the remote extension host + for (let i = this._extensionHostManagers.length - 1; i >= 0; i--) { + this._extensionHostManagers[i].dispose(); + } + this._extensionHostManagers = []; + } + + public disposeOne(extensionHostManager: IExtensionHostManager): void { + const index = this._extensionHostManagers.findIndex(el => el.extensionHost === extensionHostManager); + if (index >= 0) { + this._extensionHostManagers.splice(index, 1); + extensionHostManager.dispose(); + } + } + + public getByKind(kind: ExtensionHostKind): IExtensionHostManager[] { + return this.filter(el => el.kind === kind); + } + + public getByRunningLocation(runningLocation: ExtensionRunningLocation): IExtensionHostManager | null { + for (const el of this._extensionHostManagers) { + if (el.extensionHost.representsRunningLocation(runningLocation)) { + return el.extensionHost; + } + } + return null; + } + + *[Symbol.iterator]() { + for (const extensionHostManager of this._extensionHostManagers) { + yield extensionHostManager.extensionHost; + } + } + + public map(callback: (extHostManager: IExtensionHostManager) => T): T[] { + return this._extensionHostManagers.map(el => callback(el.extensionHost)); + } + + public every(callback: (extHostManager: IExtensionHostManager) => unknown): boolean { + return this._extensionHostManagers.every(el => callback(el.extensionHost)); + } + + public filter(callback: (extHostManager: IExtensionHostManager) => unknown): IExtensionHostManager[] { + return this._extensionHostManagers.filter(el => callback(el.extensionHost)).map(el => el.extensionHost); + } +} + +class ExtensionHostManagerData { + constructor( + public readonly extensionHost: IExtensionHostManager, + public readonly disposableStore: DisposableStore + ) { } + + public dispose(): void { + this.disposableStore.dispose(); + this.extensionHost.dispose(); + } +} + export class ResolvedExtensions { constructor( public readonly local: IExtensionDescription[], diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 3db5ef34648..a0497d7fbfd 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -10,12 +10,12 @@ import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ImplicitActivationEvents } from 'vs/platform/extensionManagement/common/implicitActivationEvents'; import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, ExtensionType, IExtension, IExtensionContributions, IExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { ApiProposalName } from 'vs/platform/extensions/common/extensionsApiProposals'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IV8Profile } from 'vs/platform/profiling/common/profiling'; import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensionHostKind'; import { IExtensionDescriptionDelta, IExtensionDescriptionSnapshot } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtensionRunningLocation } from 'vs/workbench/services/extensions/common/extensionRunningLocation'; -import { ApiProposalName } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; export const nullExtensionDescription = Object.freeze({ @@ -28,7 +28,7 @@ export const nullExtensionDescription = Object.freeze({ isBuiltin: false, targetPlatform: TargetPlatform.UNDEFINED, isUserBuiltin: false, - isUnderDevelopment: false, + isUnderDevelopment: false }); export type WebWorkerExtHostConfigValue = boolean | 'auto'; @@ -521,10 +521,12 @@ export interface IExtensionService { * @param reason a human readable reason for stopping the extension hosts. This maybe * can be presented to the user when showing dialogs. * + * @param auto indicates if the operation was triggered by an automatic action + * * @returns a promise that resolves to `true` if the extension hosts were stopped, `false` * if the operation was vetoed by listeners of the `onWillStop` event. */ - stopExtensionHosts(reason: string): Promise; + stopExtensionHosts(reason: string, auto?: boolean): Promise; /** * Starts the extension hosts. If updates are provided, the extension hosts are started with the given updates. @@ -564,16 +566,18 @@ export function toExtension(extensionDescription: IExtensionDescription): IExten } export function toExtensionDescription(extension: IExtension, isUnderDevelopment?: boolean): IExtensionDescription { + const id = getExtensionId(extension.manifest.publisher, extension.manifest.name); return { - identifier: new ExtensionIdentifier(getExtensionId(extension.manifest.publisher, extension.manifest.name)), + id, + identifier: new ExtensionIdentifier(id), isBuiltin: extension.type === ExtensionType.System, isUserBuiltin: extension.type === ExtensionType.User && extension.isBuiltin, isUnderDevelopment: !!isUnderDevelopment, extensionLocation: extension.location, - ...extension.manifest, uuid: extension.identifier.uuid, targetPlatform: extension.targetPlatform, publisherDisplayName: extension.publisherDisplayName, + ...extension.manifest }; } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts deleted file mode 100644 index 1e4390f2c62..00000000000 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ /dev/null @@ -1,130 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY. - -export const allApiProposals = Object.freeze({ - activeComment: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.activeComment.d.ts', - aiRelatedInformation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', - aiTextSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', - attributableCoverage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts', - authGetSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authGetSessions.d.ts', - authLearnMore: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authLearnMore.d.ts', - authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', - canonicalUriProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts', - chatParticipantAdditions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts', - chatParticipantPrivate: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', - chatProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', - chatTab: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatTab.d.ts', - chatVariableResolver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts', - codeActionAI: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codeActionAI.d.ts', - codeActionRanges: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codeActionRanges.d.ts', - codiconDecoration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts', - commentReactor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReactor.d.ts', - commentThreadApplicability: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts', - commentingRangeHint: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentingRangeHint.d.ts', - commentsDraftState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsDraftState.d.ts', - contribAccessibilityHelpContent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribAccessibilityHelpContent.d.ts', - contribCommentEditorActionsMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentEditorActionsMenu.d.ts', - contribCommentPeekContext: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts', - contribCommentThreadAdditionalMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts', - contribCommentsViewThreadMenus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts', - contribDiffEditorGutterToolBarMenus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribDiffEditorGutterToolBarMenus.d.ts', - contribEditSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts', - contribEditorContentMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditorContentMenu.d.ts', - contribIssueReporter: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribIssueReporter.d.ts', - contribLabelFormatterWorkspaceTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts', - contribMenuBarHome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMenuBarHome.d.ts', - contribMergeEditorMenus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMergeEditorMenus.d.ts', - contribMultiDiffEditorMenus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMultiDiffEditorMenus.d.ts', - contribNotebookStaticPreloads: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribNotebookStaticPreloads.d.ts', - contribRemoteHelp: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribRemoteHelp.d.ts', - contribShareMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribShareMenu.d.ts', - contribSourceControlHistoryItemGroupMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemGroupMenu.d.ts', - contribSourceControlHistoryItemMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemMenu.d.ts', - contribSourceControlInputBoxMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlInputBoxMenu.d.ts', - contribSourceControlTitleMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlTitleMenu.d.ts', - contribStatusBarItems: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribStatusBarItems.d.ts', - contribViewsRemote: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts', - contribViewsWelcome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts', - createFileSystemWatcher: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts', - customEditorMove: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts', - debugVisualization: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugVisualization.d.ts', - defaultChatParticipant: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts', - diffCommand: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts', - diffContentOptions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffContentOptions.d.ts', - documentFiltersExclusive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', - documentPaste: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentPaste.d.ts', - editSessionIdentityProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts', - editorHoverVerbosityLevel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts', - editorInsets: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorInsets.d.ts', - embeddings: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.embeddings.d.ts', - extensionRuntime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', - extensionsAny: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionsAny.d.ts', - externalUriOpener: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.externalUriOpener.d.ts', - fileComments: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileComments.d.ts', - fileSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts', - findFiles2: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findFiles2.d.ts', - findTextInFiles: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFiles.d.ts', - fsChunks: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fsChunks.d.ts', - idToken: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.idToken.d.ts', - inlineCompletionsAdditions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts', - inlineEdit: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineEdit.d.ts', - interactive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactive.d.ts', - interactiveWindow: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveWindow.d.ts', - ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', - languageModelSystem: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModelSystem.d.ts', - languageStatusText: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageStatusText.d.ts', - mappedEditsProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts', - multiDocumentHighlightProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts', - newSymbolNamesProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.newSymbolNamesProvider.d.ts', - notebookCellExecution: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecution.d.ts', - notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', - notebookControllerAffinityHidden: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts', - notebookDeprecated: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts', - notebookExecution: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookExecution.d.ts', - notebookKernelSource: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookKernelSource.d.ts', - notebookLiveShare: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts', - notebookMessaging: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMessaging.d.ts', - notebookMime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMime.d.ts', - notebookVariableProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts', - portsAttributes: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts', - positronResolveSymlinks: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.positronResolveSymlinks.d.ts', - profileContentHandlers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.profileContentHandlers.d.ts', - quickDiffProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts', - quickPickItemTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts', - quickPickSortByLabel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts', - resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts', - scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts', - scmHistoryProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts', - scmMultiDiffEditor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmMultiDiffEditor.d.ts', - scmSelectedProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts', - scmTextDocument: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmTextDocument.d.ts', - scmValidation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts', - shareProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.shareProvider.d.ts', - showLocal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.showLocal.d.ts', - speech: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.speech.d.ts', - tabInputMultiDiff: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts', - tabInputTextMerge: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts', - taskPresentationGroup: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts', - telemetry: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.telemetry.d.ts', - terminalDataWriteEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', - terminalDimensions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts', - terminalExecuteCommandEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalExecuteCommandEvent.d.ts', - terminalQuickFixProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts', - terminalSelection: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts', - terminalShellIntegration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts', - testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', - textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', - timeline: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts', - tokenInformation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tokenInformation.d.ts', - treeViewActiveItem: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts', - treeViewMarkdownMessage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewMarkdownMessage.d.ts', - treeViewReveal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewReveal.d.ts', - tunnelFactory: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts', - tunnels: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnels.d.ts', - workspaceTrust: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts' -}); -export type ApiProposalName = keyof typeof allApiProposals; diff --git a/src/vs/workbench/services/extensions/common/extensionsProposedApi.ts b/src/vs/workbench/services/extensions/common/extensionsProposedApi.ts index e92c5e0a50e..76d560e77b1 100644 --- a/src/vs/workbench/services/extensions/common/extensionsProposedApi.ts +++ b/src/vs/workbench/services/extensions/common/extensionsProposedApi.ts @@ -4,11 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { localize } from 'vs/nls'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ExtensionIdentifier, IExtensionDescription, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { allApiProposals, ApiProposalName } from 'vs/platform/extensions/common/extensionsApiProposals'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; +import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { ApiProposalName, allApiProposals } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; +import { Extensions, IExtensionFeatureMarkdownRenderer, IExtensionFeaturesRegistry, IRenderedData } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; +import { Mutable } from 'vs/base/common/types'; export class ExtensionsProposedApi { @@ -54,12 +61,9 @@ export class ExtensionsProposedApi { } } - private doUpdateEnabledApiProposals(_extension: IExtensionDescription): void { + private doUpdateEnabledApiProposals(extension: Mutable): void { - // this is a trick to make the extension description writeable... - type Writeable = { -readonly [P in keyof T]: Writeable }; - const extension = >_extension; - const key = ExtensionIdentifier.toKey(_extension.identifier); + const key = ExtensionIdentifier.toKey(extension.identifier); // warn about invalid proposal and remove them from the list if (isNonEmptyArray(extension.enabledApiProposals)) { @@ -110,3 +114,35 @@ export class ExtensionsProposedApi { } } } + +class ApiProposalsMarkdowneRenderer extends Disposable implements IExtensionFeatureMarkdownRenderer { + + readonly type = 'markdown'; + + shouldRender(manifest: IExtensionManifest): boolean { + return !!manifest.enabledApiProposals?.length; + } + + render(manifest: IExtensionManifest): IRenderedData { + const enabledApiProposals = manifest.enabledApiProposals || []; + const data = new MarkdownString(); + if (enabledApiProposals.length) { + for (const proposal of enabledApiProposals) { + data.appendMarkdown(`- \`${proposal}\`\n`); + } + } + return { + data, + dispose: () => { } + }; + } +} + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: 'enabledApiProposals', + label: localize('enabledProposedAPIs', "API Proposals"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(ApiProposalsMarkdowneRenderer), +}); diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index e285d1d73bd..4ff19cad7b9 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -13,10 +13,10 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IMessage } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionDescription, EXTENSION_CATEGORIES, ExtensionIdentifierSet } from 'vs/platform/extensions/common/extensions'; import { ExtensionKind } from 'vs/platform/environment/common/environment'; -import { allApiProposals } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { productSchemaId } from 'vs/platform/product/common/productService'; import { ImplicitActivationEvents, IActivationEventsGenerator } from 'vs/platform/extensionManagement/common/implicitActivationEvents'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { allApiProposals } from 'vs/platform/extensions/common/extensionsApiProposals'; const schemaRegistry = Registry.as(Extensions.JSONContribution); @@ -242,8 +242,8 @@ export const schema: IJSONSchema = { uniqueItems: true, items: { type: 'string', - enum: Object.keys(allApiProposals), - markdownEnumDescriptions: Object.values(allApiProposals) + enum: Object.keys(allApiProposals).map(proposalName => proposalName), + markdownEnumDescriptions: Object.values(allApiProposals).map(value => value.proposal) } }, api: { @@ -385,6 +385,16 @@ export const schema: IJSONSchema = { body: 'onIssueReporterOpened', description: nls.localize('vscode.extension.activationEvents.onIssueReporterOpened', 'An activation event emitted when the issue reporter is opened.'), }, + { + label: 'onChatParticipant', + body: 'onChatParticipant:${1:participantId}', + description: nls.localize('vscode.extension.activationEvents.onChatParticipant', 'An activation event emitted when the specified chat participant is invoked.'), + }, + { + label: 'onLanguageModelTool', + body: 'onLanguageModelTool:${1:toolName}', + description: nls.localize('vscode.extension.activationEvents.onLanguageModelTool', 'An activation event emitted when the specified language model tool is invoked.'), + }, { label: '*', description: nls.localize('vscode.extension.activationEvents.star', 'An activation event emitted on VS Code startup. To ensure a great end user experience, please use this activation event in your extension only when no other activation events combination works in your use-case.'), @@ -652,7 +662,7 @@ schemaRegistry.registerSchema(productSchemaId, { items: { type: 'string', enum: Object.keys(allApiProposals), - markdownEnumDescriptions: Object.values(allApiProposals) + markdownEnumDescriptions: Object.values(allApiProposals).map(value => value.proposal) } }] } diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index a1d2090c747..71d384aaa9d 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionIdentifierMap, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifierMap, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { localize } from 'vs/nls'; import { ILogService } from 'vs/platform/log/common/log'; import * as semver from 'vs/base/common/semver/semver'; +import { Mutable } from 'vs/base/common/types'; // TODO: @sandy081 merge this with deduping in extensionsScannerService.ts export function dedupExtensions(system: IExtensionDescription[], user: IExtensionDescription[], workspace: IExtensionDescription[], development: IExtensionDescription[], logService: ILogService): IExtensionDescription[] { @@ -27,7 +28,7 @@ export function dedupExtensions(system: IExtensionDescription[], user: IExtensio return; } // Overwriting a builtin extension inherits the `isBuiltin` property and it doesn't show a warning - (userExtension).isBuiltin = true; + (>userExtension).isBuiltin = true; } else { logService.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); } @@ -50,7 +51,7 @@ export function dedupExtensions(system: IExtensionDescription[], user: IExtensio if (extension) { if (extension.isBuiltin) { // Overwriting a builtin extension inherits the `isBuiltin` property - (developedExtension).isBuiltin = true; + (>developedExtension).isBuiltin = true; } } result.set(developedExtension.identifier, developedExtension); diff --git a/src/vs/workbench/services/extensions/common/rpcProtocol.ts b/src/vs/workbench/services/extensions/common/rpcProtocol.ts index e3210ebfbab..385c7cc5d1f 100644 --- a/src/vs/workbench/services/extensions/common/rpcProtocol.ts +++ b/src/vs/workbench/services/extensions/common/rpcProtocol.ts @@ -158,7 +158,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { this._unacknowledgedCount = 0; this._unresponsiveTime = 0; this._asyncCheckUresponsive = this._register(new RunOnceScheduler(() => this._checkUnresponsive(), 1000)); - this._protocol.onMessage((msg) => this._receiveOneMessage(msg)); + this._register(this._protocol.onMessage((msg) => this._receiveOneMessage(msg))); } public override dispose(): void { diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index 681138c0481..aaf716dc2bc 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -3,10 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; -import { URI } from 'vs/base/common/uri'; -import { IExtensionDescription, ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription, IExtension } from 'vs/platform/extensions/common/extensions'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { IExtensionsScannerService, IScannedExtension, toExtensionDescription as toExtensionDescriptionFromScannedExtension } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -42,11 +40,6 @@ export class CachedExtensionScanner { }); } - public async scanSingleExtension(extensionPath: string, isBuiltin: boolean): Promise { - const scannedExtension = await this._extensionsScannerService.scanExistingExtension(URI.file(path.resolve(extensionPath)), isBuiltin ? ExtensionType.System : ExtensionType.User, { language: platform.language }); - return scannedExtension ? toExtensionDescriptionFromScannedExtension(scannedExtension, false) : null; - } - public async startScanningExtensions(): Promise { try { const extensions = await this._scanInstalledExtensions(); diff --git a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts index 02f894742ad..a7958846318 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts @@ -281,7 +281,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { // Lifecycle - this._extensionHostProcess.onExit(({ code, signal }) => this._onExtHostProcessExit(code, signal)); + this._toDispose.add(this._extensionHostProcess.onExit(({ code, signal }) => this._onExtHostProcessExit(code, signal))); // Notify debugger that we are ready to attach to the process if we run a development extension if (portNumber) { diff --git a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts index cdd507e6fff..b7b77b23e0e 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts @@ -19,7 +19,7 @@ import { ConfigurationScope } from 'vs/platform/configuration/common/configurati import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ExtensionKind } from 'vs/platform/environment/common/environment'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -141,14 +141,6 @@ export class NativeExtensionService extends AbstractExtensionService implements }); } - protected _scanSingleExtension(extension: IExtension): Promise { - if (extension.location.scheme === Schemas.vscodeRemote) { - return this._remoteExtensionsScannerService.scanSingleExtension(extension.location, extension.type === ExtensionType.System); - } - - return this._extensionScanner.scanSingleExtension(extension.location.fsPath, extension.type === ExtensionType.System); - } - private async _scanAllLocalExtensions(): Promise { return this._extensionScanner.scannedExtensions; } diff --git a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts index 3ced43a5f42..69c2463958c 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; @@ -12,7 +12,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { ExtensionKind, IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ExtensionIdentifier, IExtension, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtension, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TestInstantiationService, createServices } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; @@ -209,7 +209,7 @@ suite('ExtensionService', () => { protected _resolveExtensions(): Promise { throw new Error('Method not implemented.'); } - protected _scanSingleExtension(extension: IExtension): Promise | null> { + protected _scanSingleExtension(extension: IExtension): Promise { throw new Error('Method not implemented.'); } protected _onExtensionHostExit(code: number): void { diff --git a/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts index ae0a8931944..f51e36b151a 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; diff --git a/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts b/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts index 97a4d422352..603894a3eab 100644 --- a/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts +++ b/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ExtensionIdentifier, IExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -44,7 +44,8 @@ suite('ExtensionDescriptionRegistry', () => { activationEvents, main: 'index.js', targetPlatform: TargetPlatform.UNDEFINED, - extensionDependencies: [] + extensionDependencies: [], + enabledApiProposals: undefined, }; } }); diff --git a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts index ab5a069fe14..52d7abfebcd 100644 --- a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts +++ b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts b/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts index e351f2485fe..cffda52a238 100644 --- a/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts +++ b/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts index d4c2c6f025e..4500b974e72 100644 --- a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts +++ b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts @@ -125,7 +125,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi private static readonly READONLY_MESSAGES = { providerReadonly: { value: localize('providerReadonly', "Editor is read-only because the file system of the file is read-only."), isTrusted: true }, sessionReadonly: { value: localize({ key: 'sessionReadonly', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because the file was set read-only in this session. [Click here](command:{0}) to set writeable.", 'workbench.action.files.setActiveEditorWriteableInSession'), isTrusted: true }, - configuredReadonly: { value: localize({ key: 'configuredReadonly', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because the file was set read-only via settings. [Click here](command:{0}) to configure.", `workbench.action.openSettings?${encodeURIComponent('["files.readonly"]')}`), isTrusted: true }, + configuredReadonly: { value: localize({ key: 'configuredReadonly', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because the file was set read-only via settings. [Click here](command:{0}) to configure or [toggle for this session](command:{1}).", `workbench.action.openSettings?${encodeURIComponent('["files.readonly"]')}`, 'workbench.action.files.toggleActiveEditorReadonlyInSession'), isTrusted: true }, fileLocked: { value: localize({ key: 'fileLocked', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because of file permissions. [Click here](command:{0}) to set writeable anyway.", 'workbench.action.files.setActiveEditorWriteableInSession'), isTrusted: true }, fileReadonly: { value: localize('fileReadonly', "Editor is read-only because the file is read-only."), isTrusted: true } }; diff --git a/src/vs/workbench/services/history/test/browser/historyService.test.ts b/src/vs/workbench/services/history/test/browser/historyService.test.ts index 06478095336..e00cd8ae218 100644 --- a/src/vs/workbench/services/history/test/browser/historyService.test.ts +++ b/src/vs/workbench/services/history/test/browser/historyService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor, createEditorPart, registerTestFileEditor, TestServiceAccessor, TestTextFileEditor, workbenchTeardown, registerTestSideBySideEditor } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 738e41bd672..11f6227989f 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -576,6 +576,14 @@ export class BrowserHostService extends Disposable implements IHostService { } //#endregion + + //#region File + + getPathForFile(): undefined { + return undefined; // unsupported in browser environments + } + + //#endregion } registerSingleton(IHostService, BrowserHostService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/host/browser/host.ts b/src/vs/workbench/services/host/browser/host.ts index f1586f4b9c4..724bc5a52bf 100644 --- a/src/vs/workbench/services/host/browser/host.ts +++ b/src/vs/workbench/services/host/browser/host.ts @@ -19,7 +19,6 @@ export interface IHostService { readonly _serviceBrand: undefined; - //#region Focus /** @@ -56,7 +55,6 @@ export interface IHostService { //#endregion - //#region Window /** @@ -123,4 +121,10 @@ export interface IHostService { withExpectedShutdown(expectedShutdownTask: () => Promise): Promise; //#endregion + + //#region File + + getPathForFile(file: File): string | undefined; + + //#endregion } diff --git a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts index d3fbff27c39..3163d8822b8 100644 --- a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts +++ b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -17,6 +17,7 @@ import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService'; import { disposableWindowInterval, getActiveDocument, getWindowId, getWindowsCount, hasWindow, onDidRegisterWindow } from 'vs/base/browser/dom'; import { memoize } from 'vs/base/common/decorators'; import { isAuxiliaryWindow } from 'vs/base/browser/window'; +import { webUtils } from 'vs/base/parts/sandbox/electron-sandbox/globals'; class WorkbenchNativeHostService extends NativeHostService { @@ -66,7 +67,6 @@ class WorkbenchHostService extends Disposable implements IHostService { //#endregion - //#region Window @memoize @@ -160,7 +160,6 @@ class WorkbenchHostService extends Disposable implements IHostService { //#endregion - //#region Lifecycle focus(targetWindow: Window, options?: { force: boolean }): Promise { @@ -187,6 +186,15 @@ class WorkbenchHostService extends Disposable implements IHostService { } //#endregion + + //#region File + + getPathForFile(file: File): string { + return webUtils.getPathForFile(file); + } + + //#endregion + } registerSingleton(IHostService, WorkbenchHostService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts index 0e2778b6191..7c3e693e96d 100644 --- a/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import 'vs/workbench/services/keybinding/browser/keyboardLayouts/en.darwin'; import 'vs/workbench/services/keybinding/browser/keyboardLayouts/de.darwin'; import { KeyboardLayoutContribution } from 'vs/workbench/services/keybinding/browser/keyboardLayouts/_.contribution'; diff --git a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts index 34591dbfc35..e2064decab9 100644 --- a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as json from 'vs/base/common/json'; import { KeyCode } from 'vs/base/common/keyCodes'; import { KeyCodeChord } from 'vs/base/common/keybindings'; diff --git a/src/vs/workbench/services/keybinding/test/browser/keybindingIO.test.ts b/src/vs/workbench/services/keybinding/test/browser/keybindingIO.test.ts index cfc697b817e..3f7687029a9 100644 --- a/src/vs/workbench/services/keybinding/test/browser/keybindingIO.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/keybindingIO.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyChord, KeyCode, KeyMod, ScanCode } from 'vs/base/common/keyCodes'; import { KeyCodeChord, decodeKeybinding, ScanCodeChord, Keybinding } from 'vs/base/common/keybindings'; import { KeybindingParser } from 'vs/base/common/keybindingParser'; diff --git a/src/vs/workbench/services/keybinding/test/node/keyboardMapperTestUtils.ts b/src/vs/workbench/services/keybinding/test/node/keyboardMapperTestUtils.ts index 5ee442c7285..1ef7cb70863 100644 --- a/src/vs/workbench/services/keybinding/test/node/keyboardMapperTestUtils.ts +++ b/src/vs/workbench/services/keybinding/test/node/keyboardMapperTestUtils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as path from 'vs/base/common/path'; import { SingleModifierChord, ResolvedKeybinding, Keybinding } from 'vs/base/common/keybindings'; import { Promises } from 'vs/base/node/pfs'; diff --git a/src/vs/workbench/services/keybinding/test/node/macLinuxKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/node/macLinuxKeyboardMapper.test.ts index 05a8db5fc73..7ca4e5a54fc 100644 --- a/src/vs/workbench/services/keybinding/test/node/macLinuxKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/node/macLinuxKeyboardMapper.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyChord, KeyCode, KeyMod, ScanCode, ScanCodeUtils } from 'vs/base/common/keyCodes'; import { KeyCodeChord, decodeKeybinding, createSimpleKeybinding, ScanCodeChord, Keybinding } from 'vs/base/common/keybindings'; import { UserSettingsLabelProvider } from 'vs/base/common/keybindingLabels'; diff --git a/src/vs/workbench/services/label/test/browser/label.test.ts b/src/vs/workbench/services/label/test/browser/label.test.ts index f5098c5df72..84afcb96d37 100644 --- a/src/vs/workbench/services/label/test/browser/label.test.ts +++ b/src/vs/workbench/services/label/test/browser/label.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as resources from 'vs/base/common/resources'; -import * as assert from 'assert'; +import assert from 'assert'; import { TestEnvironmentService, TestLifecycleService, TestPathService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { URI } from 'vs/base/common/uri'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; diff --git a/src/vs/workbench/services/language/common/languageService.ts b/src/vs/workbench/services/language/common/languageService.ts index a5644c2b309..1eb86cfd2d3 100644 --- a/src/vs/workbench/services/language/common/languageService.ts +++ b/src/vs/workbench/services/language/common/languageService.ts @@ -23,6 +23,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { index } from 'vs/base/common/arrays'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { isString } from 'vs/base/common/types'; export interface IRawLanguageExtensionPoint { id: string; @@ -147,6 +148,10 @@ class LanguageTableRenderer extends Disposable implements IExtensionFeatureTable const grammars = contributes?.grammars || []; grammars.forEach(grammar => { + if (!isString(grammar.language)) { + // ignore the grammars that are only used as includes in other grammars + return; + } let language = byId[grammar.language]; if (language) { @@ -160,6 +165,10 @@ class LanguageTableRenderer extends Disposable implements IExtensionFeatureTable const snippets = contributes?.snippets || []; snippets.forEach(snippet => { + if (!isString(snippet.language)) { + // ignore invalid snippets + return; + } let language = byId[snippet.language]; if (language) { diff --git a/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.test.ts b/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.test.ts index d2e590c052c..3caff2ae436 100644 --- a/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.test.ts +++ b/src/vs/workbench/services/languageRuntime/test/common/languageRuntime.test.ts @@ -3,7 +3,7 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { raceTimeout } from 'vs/base/common/async'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 4234a0985b6..7a17cb2a416 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -366,7 +366,7 @@ export function shouldShowCustomTitleBar(configurationService: IConfigurationSer } if (zenModeActive) { - return false; + return !configurationService.getValue(ZenModeSettings.FULLSCREEN); } const inFullscreen = isFullscreen(window); diff --git a/src/vs/workbench/services/lifecycle/test/electron-sandbox/lifecycleService.test.ts b/src/vs/workbench/services/lifecycle/test/electron-sandbox/lifecycleService.test.ts index 948741f9d0a..0d71a208693 100644 --- a/src/vs/workbench/services/lifecycle/test/electron-sandbox/lifecycleService.test.ts +++ b/src/vs/workbench/services/lifecycle/test/electron-sandbox/lifecycleService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; diff --git a/src/vs/workbench/services/localization/electron-sandbox/localeService.ts b/src/vs/workbench/services/localization/electron-sandbox/localeService.ts index d7124758e38..9786b3c73de 100644 --- a/src/vs/workbench/services/localization/electron-sandbox/localeService.ts +++ b/src/vs/workbench/services/localization/electron-sandbox/localeService.ts @@ -16,7 +16,7 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { localize } from 'vs/nls'; import { toAction } from 'vs/base/common/actions'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { stripComments } from 'vs/base/common/stripComments'; +import { parse } from 'vs/base/common/jsonc'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -57,7 +57,7 @@ class NativeLocaleService implements ILocaleService { // This is the same logic that we do where argv.json is parsed so mirror that: // https://github.com/microsoft/vscode/blob/32d40cf44e893e87ac33ac4f08de1e5f7fe077fc/src/main.js#L238-L246 - JSON.parse(stripComments(content.value)); + parse(content.value); } catch (error) { this.notificationService.notify({ severity: Severity.Error, diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 62f01555ef0..d5fee849ae9 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -13,7 +13,7 @@ import { IRange } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationScope, EditPresentationTypes, IExtensionInfo } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationDefaultValueSource, ConfigurationScope, EditPresentationTypes, IExtensionInfo } from 'vs/platform/configuration/common/configurationRegistry'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -91,7 +91,7 @@ export interface ISetting { enumItemLabels?: string[]; allKeysAreBoolean?: boolean; editPresentation?: EditPresentationTypes; - nonLanguageSpecificDefaultValueSource?: string | IExtensionInfo; + nonLanguageSpecificDefaultValueSource?: ConfigurationDefaultValueSource; isLanguageTagSetting?: boolean; categoryLabel?: string; diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 507844e02c8..f116a7b0067 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -16,7 +16,7 @@ import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationPropertySchema, IConfigurationRegistry, IExtensionInfo, IRegisteredConfigurationPropertySchema, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationDefaultValueSource, ConfigurationScope, Extensions, IConfigurationNode, IConfigurationPropertySchema, IConfigurationRegistry, IRegisteredConfigurationPropertySchema, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; @@ -679,7 +679,7 @@ export class DefaultSettings extends Disposable { isLanguageTagSetting = true; } - let defaultValueSource: string | IExtensionInfo | undefined; + let defaultValueSource: ConfigurationDefaultValueSource | undefined; if (!isLanguageTagSetting) { const registeredConfigurationProp = prop as IRegisteredConfigurationPropertySchema; if (registeredConfigurationProp && registeredConfigurationProp.defaultValueSource) { diff --git a/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts b/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts index af8f0deed6b..a6009748222 100644 --- a/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts +++ b/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as uuid from 'vs/base/common/uuid'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { KeyCode } from 'vs/base/common/keyCodes'; diff --git a/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts b/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts index 8dd960a0eb6..af47ad2f8ea 100644 --- a/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts +++ b/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TestCommandService } from 'vs/editor/test/browser/editorTestServices'; import { ICommandService } from 'vs/platform/commands/common/commands'; diff --git a/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts b/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts index 2b4f6f8d667..85ef6771d84 100644 --- a/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts +++ b/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { createValidator, getInvalidTypeError } from 'vs/workbench/services/preferences/common/preferencesValidation'; diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 440aaa1e544..58fe33f548f 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -27,6 +27,7 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { stripIcons } from 'vs/base/common/iconLabels'; import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; +import { IUserActivityService } from 'vs/workbench/services/userActivity/common/userActivityService'; export class ProgressService extends Disposable implements IProgressService { @@ -40,14 +41,24 @@ export class ProgressService extends Disposable implements IProgressService { @INotificationService private readonly notificationService: INotificationService, @IStatusbarService private readonly statusbarService: IStatusbarService, @ILayoutService private readonly layoutService: ILayoutService, - @IKeybindingService private readonly keybindingService: IKeybindingService + @IKeybindingService private readonly keybindingService: IKeybindingService, + @IUserActivityService private readonly userActivityService: IUserActivityService, ) { super(); } - async withProgress(options: IProgressOptions, task: (progress: IProgress) => Promise, onDidCancel?: (choice?: number) => void): Promise { + async withProgress(options: IProgressOptions, originalTask: (progress: IProgress) => Promise, onDidCancel?: (choice?: number) => void): Promise { const { location } = options; + const task = async (progress: IProgress) => { + const activeLock = this.userActivityService.markActive(); + try { + return await originalTask(progress); + } finally { + activeLock.dispose(); + } + }; + const handleStringLocation = (location: string) => { const viewContainer = this.viewDescriptorService.getViewContainerById(location); if (viewContainer) { diff --git a/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts b/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts index 16f3067679c..7bc1cec7e49 100644 --- a/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts +++ b/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AbstractProgressScope, ScopedProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; diff --git a/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts b/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts index 89e2791637e..ce9954eb03d 100644 --- a/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts +++ b/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts @@ -7,7 +7,7 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { IRemoteExtensionsScannerService, RemoteExtensionsScannerChannelName } from 'vs/platform/remote/common/remoteExtensionsScanner'; import * as platform from 'vs/base/common/platform'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IRemoteUserDataProfilesService } from 'vs/workbench/services/userDataProfile/common/remoteUserDataProfiles'; @@ -16,6 +16,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IActiveLanguagePackService } from 'vs/workbench/services/localization/common/locale'; import { IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { Mutable } from 'vs/base/common/types'; class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService { @@ -44,7 +45,7 @@ class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService return await this.withChannel( async (channel) => { const profileLocation = this.userDataProfileService.currentProfile.isDefault ? undefined : (await this.remoteUserDataProfilesService.getRemoteProfile(this.userDataProfileService.currentProfile)).extensionsResource; - const scannedExtensions = await channel.call('scanExtensions', [ + const scannedExtensions = await channel.call[]>('scanExtensions', [ platform.language, profileLocation, this.extensionManagementService.getInstalledWorkspaceExtensionLocations(), @@ -64,25 +65,6 @@ class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService } } - async scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise { - try { - return await this.withChannel( - async (channel) => { - const extension = await channel.call('scanSingleExtension', [extensionLocation, isBuiltin, platform.language]); - if (extension !== null) { - extension.extensionLocation = URI.revive(extension.extensionLocation); - // ImplicitActivationEvents.updateManifest(extension); - } - return extension; - }, - null - ); - } catch (error) { - this.logService.error(error); - return null; - } - } - private withChannel(callback: (channel: IChannel) => Promise, fallback: R): Promise { const connection = this.remoteAgentService.getConnection(); if (!connection) { diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index cb0231edff5..d03e606db74 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -29,6 +29,7 @@ export const VIEW_ID = 'workbench.view.search'; export const SEARCH_RESULT_LANGUAGE_ID = 'search-result'; export const SEARCH_EXCLUDE_CONFIG = 'search.exclude'; +export const DEFAULT_MAX_SEARCH_RESULTS = 20000; // Warning: this pattern is used in the search editor to detect offsets. If you // change this, also change the search-result built-in extension diff --git a/src/vs/workbench/services/search/common/searchExtTypesInternal.ts b/src/vs/workbench/services/search/common/searchExtTypesInternal.ts new file mode 100644 index 00000000000..37ec6163bf5 --- /dev/null +++ b/src/vs/workbench/services/search/common/searchExtTypesInternal.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import type { FileSearchOptions, TextSearchOptions } from './searchExtTypes'; + +interface RipgrepSearchOptionsCommon { + numThreads?: number; +} + +export interface RipgrepTextSearchOptions extends TextSearchOptions, RipgrepSearchOptionsCommon { } + +export interface RipgrepFileSearchOptions extends FileSearchOptions, RipgrepSearchOptionsCommon { } diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index cec7fb1114c..f4e35c525e1 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -21,7 +21,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { deserializeSearchError, FileMatch, IAITextQuery, ICachedSearchStats, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, ISearchComplete, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, isFileMatch, isProgressMessage, ITextQuery, pathIncludedInQuery, QueryType, SEARCH_RESULT_LANGUAGE_ID, SearchError, SearchErrorCode, SearchProviderType } from 'vs/workbench/services/search/common/search'; +import { DEFAULT_MAX_SEARCH_RESULTS, deserializeSearchError, FileMatch, IAITextQuery, ICachedSearchStats, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, ISearchComplete, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, isFileMatch, isProgressMessage, ITextQuery, pathIncludedInQuery, QueryType, SEARCH_RESULT_LANGUAGE_ID, SearchError, SearchErrorCode, SearchProviderType } from 'vs/workbench/services/search/common/search'; import { getTextSearchMatchWithModelContext, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; export class SearchService extends Disposable implements ISearchService { @@ -529,7 +529,7 @@ export class SearchService extends Disposable implements ISearchService { } // Use editor API to find matches - const askMax = isNumber(query.maxResults) ? query.maxResults + 1 : Number.MAX_SAFE_INTEGER; + const askMax = (isNumber(query.maxResults) ? query.maxResults : DEFAULT_MAX_SEARCH_RESULTS) + 1; let matches = model.findMatches(query.contentPattern.pattern, false, !!query.contentPattern.isRegExp, !!query.contentPattern.isCaseSensitive, query.contentPattern.isWordMatch ? query.contentPattern.wordSeparators! : null, false, askMax); if (matches.length) { if (askMax && matches.length >= askMax) { diff --git a/src/vs/workbench/services/search/common/textSearchManager.ts b/src/vs/workbench/services/search/common/textSearchManager.ts index 3abb626f64b..d135124aa05 100644 --- a/src/vs/workbench/services/search/common/textSearchManager.ts +++ b/src/vs/workbench/services/search/common/textSearchManager.ts @@ -11,7 +11,7 @@ import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { hasSiblingPromiseFn, IAITextQuery, IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, ITextSearchStats, QueryGlobTester, QueryType, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search'; +import { DEFAULT_MAX_SEARCH_RESULTS, hasSiblingPromiseFn, IAITextQuery, IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, ITextSearchStats, QueryGlobTester, QueryType, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search'; import { AITextSearchProvider, Range, TextSearchComplete, TextSearchMatch, TextSearchOptions, TextSearchProvider, TextSearchQuery, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes'; export interface IFileUtils { @@ -210,7 +210,7 @@ export class TextSearchManager { followSymlinks: !fq.ignoreSymlinks, encoding: fq.fileEncoding && this.fileUtils.toCanonicalName(fq.fileEncoding), maxFileSize: this.query.maxFileSize, - maxResults: this.query.maxResults ?? Number.MAX_SAFE_INTEGER, + maxResults: this.query.maxResults ?? DEFAULT_MAX_SEARCH_RESULTS, previewOptions: this.query.previewOptions, afterContext: this.query.afterContext, beforeContext: this.query.beforeContext diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 9b372b8dedb..a483dd848b0 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -105,7 +105,7 @@ export class FileWalker { killCmds.forEach(cmd => cmd()); } - walk(folderQueries: IFolderQuery[], extraFiles: URI[], onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgressMessage) => void, done: (error: Error | null, isLimitHit: boolean) => void): void { + walk(folderQueries: IFolderQuery[], extraFiles: URI[], numThreads: number | undefined, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgressMessage) => void, done: (error: Error | null, isLimitHit: boolean) => void): void { this.fileWalkSW = StopWatch.create(false); // Support that the file pattern is a full path to a file that exists @@ -128,7 +128,7 @@ export class FileWalker { // For each root folder this.parallel(folderQueries, (folderQuery: IFolderQuery, rootFolderDone: (err: Error | null, result: void) => void) => { - this.call(this.cmdTraversal, this, folderQuery, onResult, onMessage, (err?: Error) => { + this.call(this.cmdTraversal, this, folderQuery, numThreads, onResult, onMessage, (err?: Error) => { if (err) { const errorMessage = toErrorMessage(err); console.error(errorMessage); @@ -181,7 +181,7 @@ export class FileWalker { } } - private cmdTraversal(folderQuery: IFolderQuery, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgressMessage) => void, cb: (err?: Error) => void): void { + private cmdTraversal(folderQuery: IFolderQuery, numThreads: number | undefined, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgressMessage) => void, cb: (err?: Error) => void): void { const rootFolder = folderQuery.folder.fsPath; const isMac = platform.isMacintosh; @@ -196,7 +196,7 @@ export class FileWalker { let leftover = ''; const tree = this.initDirectoryTree(); - const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder.fsPath)!.expression); + const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder.fsPath)!.expression, numThreads); const cmd = ripgrep.cmd; const noSiblingsClauses = !Object.keys(ripgrep.siblingClauses).length; @@ -628,16 +628,18 @@ export class Engine implements ISearchEngine { private folderQueries: IFolderQuery[]; private extraFiles: URI[]; private walker: FileWalker; + private numThreads?: number; - constructor(config: IFileQuery) { + constructor(config: IFileQuery, numThreads?: number) { this.folderQueries = config.folderQueries; this.extraFiles = config.extraFileResources || []; + this.numThreads = numThreads; this.walker = new FileWalker(config); } search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgressMessage) => void, done: (error: Error | null, complete: ISearchEngineSuccess) => void): void { - this.walker.walk(this.folderQueries, this.extraFiles, onResult, onProgress, (err: Error | null, isLimitHit: boolean) => { + this.walker.walk(this.folderQueries, this.extraFiles, this.numThreads, onResult, onProgress, (err: Error | null, isLimitHit: boolean) => { done(err, { limitHit: isLimitHit, stats: this.walker.getStats(), diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index 8849a128971..61bde71b572 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -13,7 +13,7 @@ import { basename, dirname, join, sep } from 'vs/base/common/path'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ByteSize } from 'vs/platform/files/common/files'; -import { ICachedSearchStats, IFileQuery, IFileSearchProgressItem, IFileSearchStats, IFolderQuery, IProgressMessage, IRawFileMatch, IRawFileQuery, IRawQuery, IRawSearchService, IRawTextQuery, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess, isFilePatternMatch, ITextQuery } from 'vs/workbench/services/search/common/search'; +import { DEFAULT_MAX_SEARCH_RESULTS, ICachedSearchStats, IFileQuery, IFileSearchProgressItem, IFileSearchStats, IFolderQuery, IProgressMessage, IRawFileMatch, IRawFileQuery, IRawQuery, IRawSearchService, IRawTextQuery, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess, isFilePatternMatch, ITextQuery } from 'vs/workbench/services/search/common/search'; import { Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch'; import { TextSearchEngineAdapter } from 'vs/workbench/services/search/node/textSearchAdapter'; @@ -26,7 +26,7 @@ export class SearchService implements IRawSearchService { private caches: { [cacheKey: string]: Cache } = Object.create(null); - constructor(private readonly processType: IFileSearchStats['type'] = 'searchProcess') { } + constructor(private readonly processType: IFileSearchStats['type'] = 'searchProcess', private readonly getNumThreads?: () => Promise) { } fileSearch(config: IRawFileQuery): Event { let promise: CancelablePromise; @@ -34,8 +34,9 @@ export class SearchService implements IRawSearchService { const query = reviveQuery(config); const emitter = new Emitter({ onDidAddFirstListener: () => { - promise = createCancelablePromise(token => { - return this.doFileSearchWithEngine(FileSearchEngine, query, p => emitter.fire(p), token); + promise = createCancelablePromise(async token => { + const numThreads = await this.getNumThreads?.(); + return this.doFileSearchWithEngine(FileSearchEngine, query, p => emitter.fire(p), token, SearchService.BATCH_SIZE, numThreads); }); promise.then( @@ -72,9 +73,10 @@ export class SearchService implements IRawSearchService { return emitter.event; } - private ripgrepTextSearch(config: ITextQuery, progressCallback: IProgressCallback, token: CancellationToken): Promise { + private async ripgrepTextSearch(config: ITextQuery, progressCallback: IProgressCallback, token: CancellationToken): Promise { config.maxFileSize = this.getPlatformFileLimits().maxFileSize; - const engine = new TextSearchEngineAdapter(config); + const numThreads = await this.getNumThreads?.(); + const engine = new TextSearchEngineAdapter(config, numThreads); return engine.search(token, progressCallback, progressCallback); } @@ -85,11 +87,11 @@ export class SearchService implements IRawSearchService { }; } - doFileSearch(config: IFileQuery, progressCallback: IProgressCallback, token?: CancellationToken): Promise { - return this.doFileSearchWithEngine(FileSearchEngine, config, progressCallback, token); + doFileSearch(config: IFileQuery, numThreads: number | undefined, progressCallback: IProgressCallback, token?: CancellationToken): Promise { + return this.doFileSearchWithEngine(FileSearchEngine, config, progressCallback, token, SearchService.BATCH_SIZE, numThreads); } - doFileSearchWithEngine(EngineClass: { new(config: IFileQuery): ISearchEngine }, config: IFileQuery, progressCallback: IProgressCallback, token?: CancellationToken, batchSize = SearchService.BATCH_SIZE): Promise { + doFileSearchWithEngine(EngineClass: { new(config: IFileQuery, numThreads?: number | undefined): ISearchEngine }, config: IFileQuery, progressCallback: IProgressCallback, token?: CancellationToken, batchSize = SearchService.BATCH_SIZE, threads?: number): Promise { let resultCount = 0; const fileProgressCallback: IFileProgressCallback = progress => { if (Array.isArray(progress)) { @@ -107,7 +109,7 @@ export class SearchService implements IRawSearchService { let sortedSearch = this.trySortedSearchFromCache(config, fileProgressCallback, token); if (!sortedSearch) { const walkerConfig = config.maxResults ? Object.assign({}, config, { maxResults: null }) : config; - const engine = new EngineClass(walkerConfig); + const engine = new EngineClass(walkerConfig, threads); sortedSearch = this.doSortedSearch(engine, config, progressCallback, fileProgressCallback, token); } @@ -120,7 +122,7 @@ export class SearchService implements IRawSearchService { }); } - const engine = new EngineClass(config); + const engine = new EngineClass(config, threads); return this.doSearch(engine, fileProgressCallback, batchSize, token).then(complete => { return { @@ -258,7 +260,7 @@ export class SearchService implements IRawSearchService { const query = prepareQuery(config.filePattern || ''); const compare = (matchA: IRawFileMatch, matchB: IRawFileMatch) => compareItemsByFuzzyScore(matchA, matchB, query, true, FileMatchItemAccessor, scorerCache); - const maxResults = typeof config.maxResults === 'number' ? config.maxResults : Number.MAX_VALUE; + const maxResults = typeof config.maxResults === 'number' ? config.maxResults : DEFAULT_MAX_SEARCH_RESULTS; return arrays.topAsync(results, compare, maxResults, 10000, token); } diff --git a/src/vs/workbench/services/search/node/ripgrepFileSearch.ts b/src/vs/workbench/services/search/node/ripgrepFileSearch.ts index 8ad10f0ccbb..f833061c553 100644 --- a/src/vs/workbench/services/search/node/ripgrepFileSearch.ts +++ b/src/vs/workbench/services/search/node/ripgrepFileSearch.ts @@ -17,8 +17,8 @@ import { rgPath } from '@vscode/ripgrep'; // If @vscode/ripgrep is in an .asar file, then the binary is unpacked. const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked'); -export function spawnRipgrepCmd(config: IFileQuery, folderQuery: IFolderQuery, includePattern?: glob.IExpression, excludePattern?: glob.IExpression) { - const rgArgs = getRgArgs(config, folderQuery, includePattern, excludePattern); +export function spawnRipgrepCmd(config: IFileQuery, folderQuery: IFolderQuery, includePattern?: glob.IExpression, excludePattern?: glob.IExpression, numThreads?: number) { + const rgArgs = getRgArgs(config, folderQuery, includePattern, excludePattern, numThreads); const cwd = folderQuery.folder.fsPath; return { cmd: cp.spawn(rgDiskPath, rgArgs.args, { cwd }), @@ -29,7 +29,7 @@ export function spawnRipgrepCmd(config: IFileQuery, folderQuery: IFolderQuery, i }; } -function getRgArgs(config: IFileQuery, folderQuery: IFolderQuery, includePattern?: glob.IExpression, excludePattern?: glob.IExpression) { +function getRgArgs(config: IFileQuery, folderQuery: IFolderQuery, includePattern?: glob.IExpression, excludePattern?: glob.IExpression, numThreads?: number) { const args = ['--files', '--hidden', '--case-sensitive', '--no-require-git']; // includePattern can't have siblingClauses @@ -71,6 +71,10 @@ function getRgArgs(config: IFileQuery, folderQuery: IFolderQuery, includePattern args.push('--quiet'); } + if (numThreads) { + args.push('--threads', `${numThreads}`); + } + args.push('--no-config'); if (folderQuery.disregardGlobalIgnoreFiles) { args.push('--no-ignore-global'); diff --git a/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts b/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts index 56430c94287..bff10b050d6 100644 --- a/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts +++ b/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts @@ -6,27 +6,33 @@ import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { OutputChannel } from 'vs/workbench/services/search/node/ripgrepSearchUtils'; import { RipgrepTextSearchEngine } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine'; -import { TextSearchProvider, TextSearchComplete, TextSearchResult, TextSearchQuery, TextSearchOptions } from 'vs/workbench/services/search/common/searchExtTypes'; +import { TextSearchProvider, TextSearchComplete, TextSearchResult, TextSearchQuery, TextSearchOptions, } from 'vs/workbench/services/search/common/searchExtTypes'; import { Progress } from 'vs/platform/progress/common/progress'; import { Schemas } from 'vs/base/common/network'; +import type { RipgrepTextSearchOptions } from 'vs/workbench/services/search/common/searchExtTypesInternal'; export class RipgrepSearchProvider implements TextSearchProvider { private inProgress: Set = new Set(); - constructor(private outputChannel: OutputChannel) { + constructor(private outputChannel: OutputChannel, private getNumThreads: () => Promise) { process.once('exit', () => this.dispose()); } - provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Promise { - const engine = new RipgrepTextSearchEngine(this.outputChannel); + async provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Promise { + const numThreads = await this.getNumThreads(); + const engine = new RipgrepTextSearchEngine(this.outputChannel, numThreads); + const extendedOptions: RipgrepTextSearchOptions = { + ...options, + numThreads, + }; if (options.folder.scheme === Schemas.vscodeUserData) { // Ripgrep search engine can only provide file-scheme results, but we want to use it to search some schemes that are backed by the filesystem, but with some other provider as the frontend, // case in point vscode-userdata. In these cases we translate the query to a file, and translate the results back to the frontend scheme. - const translatedOptions = { ...options, folder: options.folder.with({ scheme: Schemas.file }) }; + const translatedOptions = { ...extendedOptions, folder: options.folder.with({ scheme: Schemas.file }) }; const progressTranslator = new Progress(data => progress.report({ ...data, uri: data.uri.with({ scheme: options.folder.scheme }) })); return this.withToken(token, token => engine.provideTextSearchResults(query, translatedOptions, progressTranslator, token)); } else { - return this.withToken(token, token => engine.provideTextSearchResults(query, options, progress, token)); + return this.withToken(token, token => engine.provideTextSearchResults(query, extendedOptions, progress, token)); } } diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index 727a1ba64c7..4740a661119 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -18,13 +18,14 @@ import { Range, TextSearchComplete, TextSearchContext, TextSearchMatch, TextSear import { AST as ReAST, RegExpParser, RegExpVisitor } from 'vscode-regexpp'; import { rgPath } from '@vscode/ripgrep'; import { anchorGlob, createTextSearchResult, IOutputChannel, Maybe } from './ripgrepSearchUtils'; +import type { RipgrepTextSearchOptions } from 'vs/workbench/services/search/common/searchExtTypesInternal'; // If @vscode/ripgrep is in an .asar file, then the binary is unpacked. const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked'); export class RipgrepTextSearchEngine { - constructor(private outputChannel: IOutputChannel) { } + constructor(private outputChannel: IOutputChannel, private readonly _numThreads?: number | undefined) { } provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Promise { this.outputChannel.appendLine(`provideTextSearchResults ${query.pattern}, ${JSON.stringify({ @@ -37,7 +38,11 @@ export class RipgrepTextSearchEngine { return new Promise((resolve, reject) => { token.onCancellationRequested(() => cancel()); - const rgArgs = getRgArgs(query, options); + const extendedOptions: RipgrepTextSearchOptions = { + ...options, + numThreads: this._numThreads + }; + const rgArgs = getRgArgs(query, extendedOptions); const cwd = options.folder.fsPath; @@ -368,7 +373,7 @@ function getNumLinesAndLastNewlineLength(text: string): { numLines: number; last } // exported for testing -export function getRgArgs(query: TextSearchQuery, options: TextSearchOptions): string[] { +export function getRgArgs(query: TextSearchQuery, options: RipgrepTextSearchOptions): string[] { const args = ['--hidden', '--no-require-git']; args.push(query.isCaseSensitive ? '--case-sensitive' : '--ignore-case'); @@ -422,6 +427,10 @@ export function getRgArgs(query: TextSearchQuery, options: TextSearchOptions): s args.push('--encoding', options.encoding); } + if (options.numThreads) { + args.push('--threads', `${options.numThreads}`); + } + // Ripgrep handles -- as a -- arg separator. Only --. // - is ok, --- is ok, --some-flag is also ok. Need to special case. if (query.pattern === '--') { diff --git a/src/vs/workbench/services/search/node/textSearchAdapter.ts b/src/vs/workbench/services/search/node/textSearchAdapter.ts index 45b8aab2922..58145f0af56 100644 --- a/src/vs/workbench/services/search/node/textSearchAdapter.ts +++ b/src/vs/workbench/services/search/node/textSearchAdapter.ts @@ -11,7 +11,7 @@ import { NativeTextSearchManager } from 'vs/workbench/services/search/node/textS export class TextSearchEngineAdapter { - constructor(private query: ITextQuery) { } + constructor(private query: ITextQuery, private numThreads?: number) { } search(token: CancellationToken, onResult: (matches: ISerializedFileMatch[]) => void, onMessage: (message: IProgressMessage) => void): Promise { if ((!this.query.folderQueries || !this.query.folderQueries.length) && (!this.query.extraFileResources || !this.query.extraFileResources.length)) { @@ -30,7 +30,7 @@ export class TextSearchEngineAdapter { onMessage({ message: msg }); } }; - const textSearchManager = new NativeTextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel), pfs); + const textSearchManager = new NativeTextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel, this.numThreads), pfs); return new Promise((resolve, reject) => { return textSearchManager .search( diff --git a/src/vs/workbench/services/search/test/browser/queryBuilder.test.ts b/src/vs/workbench/services/search/test/browser/queryBuilder.test.ts index 53209bfbe45..e1172336673 100644 --- a/src/vs/workbench/services/search/test/browser/queryBuilder.test.ts +++ b/src/vs/workbench/services/search/test/browser/queryBuilder.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IExpression } from 'vs/base/common/glob'; import { join } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/services/search/test/common/ignoreFile.test.ts b/src/vs/workbench/services/search/test/common/ignoreFile.test.ts index d5627df9bf1..75a456be6ec 100644 --- a/src/vs/workbench/services/search/test/common/ignoreFile.test.ts +++ b/src/vs/workbench/services/search/test/common/ignoreFile.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IgnoreFile } from 'vs/workbench/services/search/common/ignoreFile'; diff --git a/src/vs/workbench/services/search/test/common/queryBuilder.test.ts b/src/vs/workbench/services/search/test/common/queryBuilder.test.ts index a10084b5db9..a4a7ce1cf18 100644 --- a/src/vs/workbench/services/search/test/common/queryBuilder.test.ts +++ b/src/vs/workbench/services/search/test/common/queryBuilder.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/services/search/test/common/replace.test.ts b/src/vs/workbench/services/search/test/common/replace.test.ts index 5b4b721a30f..871c7e39f6f 100644 --- a/src/vs/workbench/services/search/test/common/replace.test.ts +++ b/src/vs/workbench/services/search/test/common/replace.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ReplacePattern } from 'vs/workbench/services/search/common/replace'; diff --git a/src/vs/workbench/services/search/test/common/search.test.ts b/src/vs/workbench/services/search/test/common/search.test.ts index 13fdd725441..5f0ec330f8a 100644 --- a/src/vs/workbench/services/search/test/common/search.test.ts +++ b/src/vs/workbench/services/search/test/common/search.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ITextSearchPreviewOptions, OneLineRange, TextSearchMatch, SearchRange } from 'vs/workbench/services/search/common/search'; diff --git a/src/vs/workbench/services/search/test/common/searchHelpers.test.ts b/src/vs/workbench/services/search/test/common/searchHelpers.test.ts index bc51969faef..2d9db98c70d 100644 --- a/src/vs/workbench/services/search/test/common/searchHelpers.test.ts +++ b/src/vs/workbench/services/search/test/common/searchHelpers.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { FindMatch, ITextModel } from 'vs/editor/common/model'; diff --git a/src/vs/workbench/services/search/test/node/fileSearch.integrationTest.ts b/src/vs/workbench/services/search/test/node/fileSearch.integrationTest.ts index ac3dd097c76..3cfd50a70f5 100644 --- a/src/vs/workbench/services/search/test/node/fileSearch.integrationTest.ts +++ b/src/vs/workbench/services/search/test/node/fileSearch.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileAccess } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; @@ -25,11 +25,13 @@ const MULTIROOT_QUERIES: IFolderQuery[] = [ { folder: URI.file(MORE_FIXTURES) } ]; +const numThreads = undefined; + async function doSearchTest(query: IFileQuery, expectedResultCount: number | Function): Promise { const svc = new SearchService(); const results: ISerializedSearchProgressItem[] = []; - await svc.doFileSearch(query, e => { + await svc.doFileSearch(query, numThreads, e => { if (!isProgressMessage(e)) { if (Array.isArray(e)) { results.push(...e); diff --git a/src/vs/workbench/services/search/test/node/rawSearchService.integrationTest.ts b/src/vs/workbench/services/search/test/node/rawSearchService.integrationTest.ts index 1838e82c12d..7c2a5407a37 100644 --- a/src/vs/workbench/services/search/test/node/rawSearchService.integrationTest.ts +++ b/src/vs/workbench/services/search/test/node/rawSearchService.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/services/search/test/node/ripgrepFileSearch.test.ts b/src/vs/workbench/services/search/test/node/ripgrepFileSearch.test.ts index fae2e5b6c4c..98d8e8a421d 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepFileSearch.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepFileSearch.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as platform from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { fixDriveC, getAbsoluteGlob } from 'vs/workbench/services/search/node/ripgrepFileSearch'; diff --git a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts index 09f3320cf28..e93c4efb269 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { fixRegexNewline, IRgMatch, IRgMessage, RipgrepParser, unicodeEscapesToPCRE2, fixNewline, getRgArgs, performBraceExpansionForRipgrep } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine'; diff --git a/src/vs/workbench/services/search/test/node/search.integrationTest.ts b/src/vs/workbench/services/search/test/node/search.integrationTest.ts index cc9b541e704..ae3a4b4fe74 100644 --- a/src/vs/workbench/services/search/test/node/search.integrationTest.ts +++ b/src/vs/workbench/services/search/test/node/search.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as path from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; import { joinPath } from 'vs/base/common/resources'; diff --git a/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts b/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts index 206de297464..503b314ffbe 100644 --- a/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts +++ b/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as path from 'vs/base/common/path'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as glob from 'vs/base/common/glob'; diff --git a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts index 693c4e9f0c0..1eb64535a8d 100644 --- a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts +++ b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/services/statusbar/browser/statusbar.ts b/src/vs/workbench/services/statusbar/browser/statusbar.ts index 1d00e77bca7..73fac7bb924 100644 --- a/src/vs/workbench/services/statusbar/browser/statusbar.ts +++ b/src/vs/workbench/services/statusbar/browser/statusbar.ts @@ -174,9 +174,9 @@ export interface IStatusbarEntry { /** * Will enable a spinning icon in front of the text to indicate progress. When `true` is - * specified, `syncing` will be used. + * specified, `loading` will be used. */ - readonly showProgress?: boolean | 'syncing' | 'loading'; + readonly showProgress?: boolean | 'loading' | 'syncing'; /** * The kind of status bar entry. This applies different colors to the entry. diff --git a/src/vs/workbench/services/suggest/browser/media/suggest.css b/src/vs/workbench/services/suggest/browser/media/suggest.css index cf39f56b816..b540839efa3 100644 --- a/src/vs/workbench/services/suggest/browser/media/suggest.css +++ b/src/vs/workbench/services/suggest/browser/media/suggest.css @@ -8,9 +8,8 @@ * layer breakers and shipping with the standalone monaco editor. */ -/* TODO: Position correctly */ .workbench-suggest-widget { - position: absolute; + position: fixed; left: 0; top: 0; } diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts index 3aa43e4c727..fd3b2131cf5 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts @@ -38,6 +38,6 @@ export class SimpleCompletionItem { readonly completion: ISimpleCompletion ) { // ensure lower-variants (perf) - this.labelLow = this.completion.label.toLowerCase(); + this.labelLow = (this.completion.completionText ?? this.completion.label).toLowerCase(); } } diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts index 1418e462f00..628ee0a809e 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts @@ -165,7 +165,7 @@ export class SimpleCompletionModel { } else { // by default match `word` against the `label` - const match = scoreFn(word, wordLow, wordPos, item.completion.label, item.labelLow, 0, this._fuzzyScoreOptions); + const match = scoreFn(word, wordLow, wordPos, item.completion.completionText ?? item.completion.label, item.labelLow, 0, this._fuzzyScoreOptions); if (!match) { continue; // NO match } @@ -177,7 +177,7 @@ export class SimpleCompletionModel { target.push(item); // update stats - labelLengths.push(item.completion.label.length); + labelLengths.push((item.completion.completionText ?? item.completion.label).length); } this._filteredItems = target.sort((a, b) => b.score[0] - a.score[0]); diff --git a/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts index 950605ae825..c11cf658514 100644 --- a/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { resolveWorkbenchCommonProperties } from 'vs/workbench/services/telemetry/browser/workbenchCommonProperties'; import { InMemoryStorageService } from 'vs/platform/storage/common/storage'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts index e51736b4ee9..bb4004c5419 100644 --- a/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { release, hostname } from 'os'; import { resolveWorkbenchCommonProperties } from 'vs/workbench/services/telemetry/common/workbenchCommonProperties'; import { StorageScope, InMemoryStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; diff --git a/src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateWorkerTokenizer.ts b/src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateWorkerTokenizer.ts index bc98f7239ce..112d76ec0d9 100644 --- a/src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateWorkerTokenizer.ts +++ b/src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateWorkerTokenizer.ts @@ -19,6 +19,7 @@ import { TokenizationSupportWithLineLimit } from 'vs/workbench/services/textMate import type { StackDiff, StateStack, diffStateStacksRefEq } from 'vscode-textmate'; import { ICreateGrammarResult } from 'vs/workbench/services/textMate/common/TMGrammarFactory'; import { StateDeltas } from 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker'; +import { Disposable } from 'vs/base/common/lifecycle'; export interface TextMateModelTokenizerHost { getOrCreateGrammar(languageId: string, encodedLanguageId: LanguageId): Promise; @@ -98,6 +99,7 @@ export class TextMateWorkerTokenizer extends MirrorTextModel { }, false ), + Disposable.None, this._maxTokenizationLineLength ); this._tokenizerWithStateStore = new TokenizerWithStateStore(this._lines.length, tokenizationSupport); diff --git a/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts b/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts index 74a52fae9ff..02eef664f6f 100644 --- a/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts +++ b/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts @@ -85,9 +85,9 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate this._updateTheme(this._themeService.getColorTheme(), false); })); - this._languageService.onDidRequestRichLanguageFeatures((languageId) => { + this._register(this._languageService.onDidRequestRichLanguageFeatures((languageId) => { this._createdModes.push(languageId); - }); + })); } private getAsyncTokenizationEnabled(): boolean { @@ -171,7 +171,7 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate } } - const validLanguageId = grammar.language && this._languageService.isRegisteredLanguageId(grammar.language) ? grammar.language : null; + const validLanguageId = grammar.language && this._languageService.isRegisteredLanguageId(grammar.language) ? grammar.language : undefined; function asStringArray(array: unknown, defaultValue: string[]): string[] { if (!Array.isArray(array)) { @@ -185,7 +185,7 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate return { location: grammarLocation, - language: validLanguageId || undefined, + language: validLanguageId, scopeName: grammar.scopeName, embeddedLanguages: embeddedLanguages, tokenTypes: tokenTypes, @@ -302,14 +302,14 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate }, true, ); - tokenization.onDidEncounterLanguage((encodedLanguageId) => { + const disposable = tokenization.onDidEncounterLanguage((encodedLanguageId) => { if (!this._encounteredLanguages[encodedLanguageId]) { const languageId = this._languageService.languageIdCodec.decodeLanguageId(encodedLanguageId); this._encounteredLanguages[encodedLanguageId] = true; this._languageService.requestBasicLanguageFeatures(languageId); } }); - return new TokenizationSupportWithLineLimit(encodedLanguageId, tokenization, maxTokenizationLineLength); + return new TokenizationSupportWithLineLimit(encodedLanguageId, tokenization, disposable, maxTokenizationLineLength); } catch (err) { if (err.message && err.message === missingTMGrammarErrorMessage) { // Don't log this error message diff --git a/src/vs/workbench/services/textMate/browser/tokenizationSupport/tokenizationSupportWithLineLimit.ts b/src/vs/workbench/services/textMate/browser/tokenizationSupport/tokenizationSupportWithLineLimit.ts index f5bd00e965e..a32113c6e8e 100644 --- a/src/vs/workbench/services/textMate/browser/tokenizationSupport/tokenizationSupportWithLineLimit.ts +++ b/src/vs/workbench/services/textMate/browser/tokenizationSupport/tokenizationSupportWithLineLimit.ts @@ -7,7 +7,7 @@ import { LanguageId } from 'vs/editor/common/encodedTokenAttributes'; import { EncodedTokenizationResult, IBackgroundTokenizationStore, IBackgroundTokenizer, IState, ITokenizationSupport, TokenizationResult } from 'vs/editor/common/languages'; import { nullTokenizeEncoded } from 'vs/editor/common/languages/nullTokenize'; import { ITextModel } from 'vs/editor/common/model'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IObservable, keepObserved } from 'vs/base/common/observable'; export class TokenizationSupportWithLineLimit extends Disposable implements ITokenizationSupport { @@ -18,11 +18,13 @@ export class TokenizationSupportWithLineLimit extends Disposable implements ITok constructor( private readonly _encodedLanguageId: LanguageId, private readonly _actual: ITokenizationSupport, + disposable: IDisposable, private readonly _maxTokenizationLineLength: IObservable, ) { super(); this._register(keepObserved(this._maxTokenizationLineLength)); + this._register(disposable); } getInitialState(): IState { diff --git a/src/vs/workbench/services/textMate/common/TMGrammars.ts b/src/vs/workbench/services/textMate/common/TMGrammars.ts index b460653c97b..150a91d9120 100644 --- a/src/vs/workbench/services/textMate/common/TMGrammars.ts +++ b/src/vs/workbench/services/textMate/common/TMGrammars.ts @@ -16,7 +16,7 @@ export interface TokenTypesContribution { } export interface ITMSyntaxExtensionPoint { - language: string; + language?: string; // undefined if the grammar is only included by other grammars scopeName: string; path: string; embeddedLanguages: IEmbeddedLanguagesMap; diff --git a/src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts b/src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts index 917849e3b57..3844a0f51d1 100644 --- a/src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts +++ b/src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ArrayEdit, MonotonousIndexTransformer, SingleArrayEdit } from 'vs/workbench/services/textMate/browser/arrayOperation'; diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index b388e7cc334..859fa8a1c1a 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -304,7 +304,12 @@ export abstract class AbstractTextFileService extends Disposable implements ITex // read through encoding library return toDecodeStream(stream, { acceptTextOnly: options?.acceptTextOnly ?? false, - guessEncoding: options?.autoGuessEncoding || this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'), + guessEncoding: + options?.autoGuessEncoding || + this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'), + candidateGuessEncodings: + options?.candidateGuessEncodings || + this.textResourceConfigurationService.getValue(resource, 'files.candidateGuessEncodings'), overwriteEncoding: async detectedEncoding => { const { encoding } = await this.encoding.getPreferredReadEncoding(resource, options, detectedEncoding ?? undefined); diff --git a/src/vs/workbench/services/textfile/common/encoding.ts b/src/vs/workbench/services/textfile/common/encoding.ts index ad67fb4f422..bae27136de4 100644 --- a/src/vs/workbench/services/textfile/common/encoding.ts +++ b/src/vs/workbench/services/textfile/common/encoding.ts @@ -7,6 +7,7 @@ import { Readable, ReadableStream, newWriteableStream, listenStream } from 'vs/b import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { importAMDNodeModule } from 'vs/amdX'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { coalesce } from 'vs/base/common/arrays'; export const UTF8 = 'utf8'; export const UTF8_with_bom = 'utf8bom'; @@ -31,6 +32,7 @@ const AUTO_ENCODING_GUESS_MAX_BYTES = 512 * 128; // set an upper limit for the export interface IDecodeStreamOptions { acceptTextOnly: boolean; guessEncoding: boolean; + candidateGuessEncodings: string[]; minBytesRequiredForDetection?: number; overwriteEncoding(detectedEncoding: string | null): Promise; @@ -134,7 +136,7 @@ export function toDecodeStream(source: VSBufferReadableStream, options: IDecodeS const detected = await detectEncodingFromBuffer({ buffer: VSBuffer.concat(bufferedChunks), bytesRead: bytesBuffered - }, options.guessEncoding); + }, options.guessEncoding, options.candidateGuessEncodings); // throw early if the source seems binary and // we are instructed to only accept text @@ -317,7 +319,7 @@ const IGNORE_ENCODINGS = ['ascii', 'utf-16', 'utf-32']; /** * Guesses the encoding from buffer. */ -async function guessEncodingByBuffer(buffer: VSBuffer): Promise { +async function guessEncodingByBuffer(buffer: VSBuffer, candidateGuessEncodings?: string[]): Promise { const jschardet = await importAMDNodeModule('jschardet', 'dist/jschardet.min.js'); // ensure to limit buffer for guessing due to https://github.com/aadsm/jschardet/issues/53 @@ -328,7 +330,15 @@ async function guessEncodingByBuffer(buffer: VSBuffer): Promise { // https://github.com/aadsm/jschardet/blob/v2.1.1/src/index.js#L36-L40 const binaryString = encodeLatin1(limitedBuffer.buffer); - const guessed = jschardet.detect(binaryString); + // ensure to convert candidate encodings to jschardet encoding names if provided + if (candidateGuessEncodings) { + candidateGuessEncodings = coalesce(candidateGuessEncodings.map(e => toJschardetEncoding(e))); + if (candidateGuessEncodings.length === 0) { + candidateGuessEncodings = undefined; + } + } + + const guessed = jschardet.detect(binaryString, candidateGuessEncodings ? { detectEncodings: candidateGuessEncodings } : undefined); if (!guessed || !guessed.encoding) { return null; } @@ -346,13 +356,24 @@ const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = { 'big5': 'cp950' }; +function normalizeEncoding(encodingName: string): string { + return encodingName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); +} + function toIconvLiteEncoding(encodingName: string): string { - const normalizedEncodingName = encodingName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); + const normalizedEncodingName = normalizeEncoding(encodingName); const mapped = JSCHARDET_TO_ICONV_ENCODINGS[normalizedEncodingName]; return mapped || normalizedEncodingName; } +function toJschardetEncoding(encodingName: string): string | undefined { + const normalizedEncodingName = normalizeEncoding(encodingName); + const mapped = GUESSABLE_ENCODINGS[normalizedEncodingName]; + + return mapped.guessableName; +} + function encodeLatin1(buffer: Uint8Array): string { let result = ''; for (let i = 0; i < buffer.length; i++) { @@ -410,9 +431,9 @@ export interface IReadResult { bytesRead: number; } -export function detectEncodingFromBuffer(readResult: IReadResult, autoGuessEncoding?: false): IDetectedEncodingResult; -export function detectEncodingFromBuffer(readResult: IReadResult, autoGuessEncoding?: boolean): Promise; -export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, autoGuessEncoding?: boolean): Promise | IDetectedEncodingResult { +export function detectEncodingFromBuffer(readResult: IReadResult, autoGuessEncoding?: false, candidateGuessEncodings?: string[]): IDetectedEncodingResult; +export function detectEncodingFromBuffer(readResult: IReadResult, autoGuessEncoding?: boolean, candidateGuessEncodings?: string[]): Promise; +export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, autoGuessEncoding?: boolean, candidateGuessEncodings?: string[]): Promise | IDetectedEncodingResult { // Always first check for BOM to find out about encoding let encoding = detectEncodingByBOMFromBuffer(buffer, bytesRead); @@ -469,7 +490,7 @@ export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, aut // Auto guess encoding if configured if (autoGuessEncoding && !seemsBinary && !encoding && buffer) { - return guessEncodingByBuffer(buffer.slice(0, bytesRead)).then(guessedEncoding => { + return guessEncodingByBuffer(buffer.slice(0, bytesRead), candidateGuessEncodings).then(guessedEncoding => { return { seemsBinary: false, encoding: guessedEncoding @@ -480,12 +501,15 @@ export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, aut return { seemsBinary, encoding }; } -export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; labelShort: string; order: number; encodeOnly?: boolean; alias?: string } } = { +type EncodingsMap = { [encoding: string]: { labelLong: string; labelShort: string; order: number; encodeOnly?: boolean; alias?: string; guessableName?: string } }; + +export const SUPPORTED_ENCODINGS: EncodingsMap = { utf8: { labelLong: 'UTF-8', labelShort: 'UTF-8', order: 1, - alias: 'utf8bom' + alias: 'utf8bom', + guessableName: 'UTF-8' }, utf8bom: { labelLong: 'UTF-8 with BOM', @@ -497,17 +521,20 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab utf16le: { labelLong: 'UTF-16 LE', labelShort: 'UTF-16 LE', - order: 3 + order: 3, + guessableName: 'UTF-16LE' }, utf16be: { labelLong: 'UTF-16 BE', labelShort: 'UTF-16 BE', - order: 4 + order: 4, + guessableName: 'UTF-16BE' }, windows1252: { labelLong: 'Western (Windows 1252)', labelShort: 'Windows 1252', - order: 5 + order: 5, + guessableName: 'windows-1252' }, iso88591: { labelLong: 'Western (ISO 8859-1)', @@ -562,12 +589,14 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab windows1250: { labelLong: 'Central European (Windows 1250)', labelShort: 'Windows 1250', - order: 16 + order: 16, + guessableName: 'windows-1250' }, iso88592: { labelLong: 'Central European (ISO 8859-2)', labelShort: 'ISO 8859-2', - order: 17 + order: 17, + guessableName: 'ISO-8859-2' }, cp852: { labelLong: 'Central European (CP 852)', @@ -577,22 +606,26 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab windows1251: { labelLong: 'Cyrillic (Windows 1251)', labelShort: 'Windows 1251', - order: 19 + order: 19, + guessableName: 'windows-1251' }, cp866: { labelLong: 'Cyrillic (CP 866)', labelShort: 'CP 866', - order: 20 + order: 20, + guessableName: 'IBM866' }, iso88595: { labelLong: 'Cyrillic (ISO 8859-5)', labelShort: 'ISO 8859-5', - order: 21 + order: 21, + guessableName: 'ISO-8859-5' }, koi8r: { labelLong: 'Cyrillic (KOI8-R)', labelShort: 'KOI8-R', - order: 22 + order: 22, + guessableName: 'KOI8-R' }, koi8u: { labelLong: 'Cyrillic (KOI8-U)', @@ -607,22 +640,26 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab windows1253: { labelLong: 'Greek (Windows 1253)', labelShort: 'Windows 1253', - order: 25 + order: 25, + guessableName: 'windows-1253' }, iso88597: { labelLong: 'Greek (ISO 8859-7)', labelShort: 'ISO 8859-7', - order: 26 + order: 26, + guessableName: 'ISO-8859-7' }, windows1255: { labelLong: 'Hebrew (Windows 1255)', labelShort: 'Windows 1255', - order: 27 + order: 27, + guessableName: 'windows-1255' }, iso88598: { labelLong: 'Hebrew (ISO 8859-8)', labelShort: 'ISO 8859-8', - order: 28 + order: 28, + guessableName: 'ISO-8859-8' }, iso885910: { labelLong: 'Nordic (ISO 8859-10)', @@ -662,7 +699,8 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab cp950: { labelLong: 'Traditional Chinese (Big5)', labelShort: 'Big5', - order: 36 + order: 36, + guessableName: 'Big5' }, big5hkscs: { labelLong: 'Traditional Chinese (Big5-HKSCS)', @@ -672,17 +710,20 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab shiftjis: { labelLong: 'Japanese (Shift JIS)', labelShort: 'Shift JIS', - order: 38 + order: 38, + guessableName: 'SHIFT_JIS' }, eucjp: { labelLong: 'Japanese (EUC-JP)', labelShort: 'EUC-JP', - order: 39 + order: 39, + guessableName: 'EUC-JP' }, euckr: { labelLong: 'Korean (EUC-KR)', labelShort: 'EUC-KR', - order: 40 + order: 40, + guessableName: 'EUC-KR' }, windows874: { labelLong: 'Thai (Windows 874)', @@ -707,7 +748,8 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab gb2312: { labelLong: 'Simplified Chinese (GB 2312)', labelShort: 'GB 2312', - order: 45 + order: 45, + guessableName: 'GB2312' }, cp865: { labelLong: 'Nordic DOS (CP 865)', @@ -720,3 +762,14 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab order: 47 } }; + +export const GUESSABLE_ENCODINGS: EncodingsMap = (() => { + const guessableEncodings: EncodingsMap = {}; + for (const encoding in SUPPORTED_ENCODINGS) { + if (SUPPORTED_ENCODINGS[encoding].guessableName) { + guessableEncodings[encoding] = SUPPORTED_ENCODINGS[encoding]; + } + } + + return guessableEncodings; +})(); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index b127ae7b7ca..3150fdd7b1d 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -33,6 +33,7 @@ import { IAccessibilityService } from 'vs/platform/accessibility/common/accessib import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IProgress, IProgressService, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress'; interface IBackupMetaData extends IWorkingCopyBackupMeta { mtime: number; @@ -123,7 +124,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil @ILanguageDetectionService languageDetectionService: ILanguageDetectionService, @IAccessibilityService accessibilityService: IAccessibilityService, @IPathService private readonly pathService: IPathService, - @IExtensionService private readonly extensionService: IExtensionService + @IExtensionService private readonly extensionService: IExtensionService, + @IProgressService private readonly progressService: IProgressService ) { super(modelService, languageService, languageDetectionService, accessibilityService); @@ -756,7 +758,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil options.reason = SaveReason.EXPLICIT; } - let versionId = this.versionId; + const versionId = this.versionId; this.trace(`doSave(${versionId}) - enter with versionId ${versionId}`); // Return early if saved from within save participant to break recursion @@ -818,6 +820,21 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil const saveCancellation = new CancellationTokenSource(); + return this.progressService.withProgress({ + title: localize('saveParticipants', "Saving '{0}'", this.name), + location: ProgressLocation.Window, + cancellable: true, + delay: this.isDirty() ? 3000 : 5000 + }, progress => { + return this.doSaveSequential(versionId, options, progress, saveCancellation); + }, () => { + saveCancellation.cancel(); + }).finally(() => { + saveCancellation.dispose(); + }); + } + + private doSaveSequential(versionId: number, options: ITextFileSaveAsOptions, progress: IProgress, saveCancellation: CancellationTokenSource): Promise { return this.saveSequentializer.run(versionId, (async () => { // A save participant can still change the model now and since we are so close to saving @@ -852,7 +869,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil if (!saveCancellation.token.isCancellationRequested) { this.ignoreSaveFromSaveParticipants = true; try { - await this.textFileService.files.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT, savedFrom: options.from }, saveCancellation.token); + await this.textFileService.files.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT, savedFrom: options.from }, progress, saveCancellation.token); } finally { this.ignoreSaveFromSaveParticipants = false; } @@ -898,6 +915,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Save to Disk. We mark the save operation as currently running with // the latest versionId because it might have changed from a save // participant triggering + progress.report({ message: localize('saveTextFile', "Writing into file...") }); this.trace(`doSave(${versionId}) - before write()`); const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); const resolvedTextFileEditorModel = this; diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 24f0d458459..773ee118d54 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -24,6 +24,7 @@ import { extname, joinPath } from 'vs/base/common/resources'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { PLAINTEXT_EXTENSION, PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager { @@ -540,8 +541,8 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE return this.saveParticipants.addSaveParticipant(participant); } - runSaveParticipants(model: ITextFileEditorModel, context: IStoredFileWorkingCopySaveParticipantContext, token: CancellationToken): Promise { - return this.saveParticipants.participate(model, context, token); + runSaveParticipants(model: ITextFileEditorModel, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { + return this.saveParticipants.participate(model, context, progress, token); } //#endregion diff --git a/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts b/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts index 97d74549b9a..e9430fd80ad 100644 --- a/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts +++ b/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { raceCancellation } from 'vs/base/common/async'; -import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; import { ITextFileSaveParticipant, ITextFileEditorModel, ITextFileSaveParticipantContext } from 'vs/workbench/services/textfile/common/textfiles'; import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { insert } from 'vs/base/common/arrays'; @@ -17,7 +16,6 @@ export class TextFileSaveParticipant extends Disposable { private readonly saveParticipants: ITextFileSaveParticipant[] = []; constructor( - @IProgressService private readonly progressService: IProgressService, @ILogService private readonly logService: ILogService ) { super(); @@ -29,40 +27,26 @@ export class TextFileSaveParticipant extends Disposable { return toDisposable(() => remove()); } - participate(model: ITextFileEditorModel, context: ITextFileSaveParticipantContext, token: CancellationToken): Promise { - const cts = new CancellationTokenSource(token); + async participate(model: ITextFileEditorModel, context: ITextFileSaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { - return this.progressService.withProgress({ - title: localize('saveParticipants', "Saving '{0}'", model.name), - location: ProgressLocation.Notification, - cancellable: true, - delay: model.isDirty() ? 3000 : 5000 - }, async progress => { + // undoStop before participation + model.textEditorModel?.pushStackElement(); - // undoStop before participation - model.textEditorModel?.pushStackElement(); - - for (const saveParticipant of this.saveParticipants) { - if (cts.token.isCancellationRequested || !model.textEditorModel /* disposed */) { - break; - } + for (const saveParticipant of this.saveParticipants) { + if (token.isCancellationRequested || !model.textEditorModel /* disposed */) { + break; + } - try { - const promise = saveParticipant.participate(model, context, progress, cts.token); - await raceCancellation(promise, cts.token); - } catch (err) { - this.logService.error(err); - } + try { + const promise = saveParticipant.participate(model, context, progress, token); + await raceCancellation(promise, token); + } catch (err) { + this.logService.error(err); } + } - // undoStop after participation - model.textEditorModel?.pushStackElement(); - }, () => { - // user cancel - cts.cancel(); - }).finally(() => { - cts.dispose(); - }); + // undoStop after participation + model.textEditorModel?.pushStackElement(); } override dispose(): void { diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index d126c88df6e..d33c93d5d54 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -131,6 +131,11 @@ export interface IReadTextFileEncodingOptions { */ readonly autoGuessEncoding?: boolean; + /** + * The optional candidateGuessEncodings parameter limits the allowed encodings to guess from. + */ + readonly candidateGuessEncodings?: string[]; + /** * The optional acceptTextOnly parameter allows to fail this request early if the file * contents are not textual. @@ -385,7 +390,7 @@ export interface ITextFileEditorModelManager { /** * Runs the registered save participants on the provided model. */ - runSaveParticipants(model: ITextFileEditorModel, context: ITextFileSaveParticipantContext, token: CancellationToken): Promise; + runSaveParticipants(model: ITextFileEditorModel, context: ITextFileSaveParticipantContext, progress: IProgress, token: CancellationToken): Promise; /** * Waits for the model to be ready to be disposed. There may be conditions diff --git a/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts b/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts index 749f3cc6dc5..af8317daf52 100644 --- a/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { IResourceDiffEditorInput, IResourceSideBySideEditorInput, isResourceDiffEditorInput, isResourceSideBySideEditorInput, isUntitledResourceEditorInput } from 'vs/workbench/common/editor'; import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, registerTestResourceEditor, registerTestSideBySideEditor } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.integrationTest.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.integrationTest.ts index 866265c14fa..622f4e90151 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.integrationTest.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index 069b90a09d9..e7d56251018 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { EncodingMode, TextFileEditorModelState, snapshotToString, isTextFileEditorModel, ITextFileEditorModelSaveEvent } from 'vs/workbench/services/textfile/common/textfiles'; diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts index 6cbeccc8876..8c3c0574bf6 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService, TestServiceAccessor, ITestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts index e3f16f16065..a1cc6f8bafc 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { workbenchInstantiationService, TestServiceAccessor, ITestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices'; import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/services/textfile/test/common/fixtures/files.ts b/src/vs/workbench/services/textfile/test/common/fixtures/files.ts index f3d0c5d6cd2..3167d521985 100644 --- a/src/vs/workbench/services/textfile/test/common/fixtures/files.ts +++ b/src/vs/workbench/services/textfile/test/common/fixtures/files.ts @@ -68,6 +68,9 @@ fixtures['index.html'] = Uint8Array.from([ 60, 33, 68, 79, 67, 84, 89, 80, 69, 32, 104, 116, 109, 108, 62, 10, 60, 104, 116, 109, 108, 62, 10, 60, 104, 101, 97, 100, 32, 105, 100, 61, 39, 104, 101, 97, 100, 73, 68, 39, 62, 10, 32, 32, 32, 32, 60, 109, 101, 116, 97, 32, 104, 116, 116, 112, 45, 101, 113, 117, 105, 118, 61, 34, 88, 45, 85, 65, 45, 67, 111, 109, 112, 97, 116, 105, 98, 108, 101, 34, 32, 99, 111, 110, 116, 101, 110, 116, 61, 34, 73, 69, 61, 101, 100, 103, 101, 34, 32, 47, 62, 10, 32, 32, 32, 32, 60, 116, 105, 116, 108, 101, 62, 83, 116, 114, 97, 100, 97, 32, 60, 47, 116, 105, 116, 108, 101, 62, 10, 32, 32, 32, 32, 60, 108, 105, 110, 107, 32, 104, 114, 101, 102, 61, 34, 115, 105, 116, 101, 46, 99, 115, 115, 34, 32, 114, 101, 108, 61, 34, 115, 116, 121, 108, 101, 115, 104, 101, 101, 116, 34, 32, 116, 121, 112, 101, 61, 34, 116, 101, 120, 116, 47, 99, 115, 115, 34, 32, 47, 62, 10, 32, 32, 32, 32, 60, 115, 99, 114, 105, 112, 116, 32, 115, 114, 99, 61, 34, 106, 113, 117, 101, 114, 121, 45, 49, 46, 52, 46, 49, 46, 106, 115, 34, 62, 60, 47, 115, 99, 114, 105, 112, 116, 62, 10, 32, 32, 32, 32, 60, 115, 99, 114, 105, 112, 116, 32, 115, 114, 99, 61, 34, 46, 46, 47, 99, 111, 109, 112, 105, 108, 101, 114, 47, 100, 116, 114, 101, 101, 46, 106, 115, 34, 32, 116, 121, 112, 101, 61, 34, 116, 101, 120, 116, 47, 106, 97, 118, 97, 115, 99, 114, 105, 112, 116, 34, 62, 60, 47, 115, 99, 114, 105, 112, 116, 62, 10, 32, 32, 32, 32, 60, 115, 99, 114, 105, 112, 116, 32, 115, 114, 99, 61, 34, 46, 46, 47, 99, 111, 109, 112, 105, 108, 101, 114, 47, 116, 121, 112, 101, 115, 99, 114, 105, 112, 116, 46, 106, 115, 34, 32, 116, 121, 112, 101, 61, 34, 116, 101, 120, 116, 47, 106, 97, 118, 97, 115, 99, 114, 105, 112, 116, 34, 62, 60, 47, 115, 99, 114, 105, 112, 116, 62, 10, 32, 32, 32, 32, 60, 115, 99, 114, 105, 112, 116, 32, 116, 121, 112, 101, 61, 34, 116, 101, 120, 116, 47, 106, 97, 118, 97, 115, 99, 114, 105, 112, 116, 34, 62, 10, 10, 32, 32, 32, 32, 47, 47, 32, 67, 111, 109, 112, 105, 108, 101, 32, 115, 116, 114, 97, 100, 97, 32, 115, 111, 117, 114, 99, 101, 32, 105, 110, 116, 111, 32, 114, 101, 115, 117, 108, 116, 105, 110, 103, 32, 106, 97, 118, 97, 115, 99, 114, 105, 112, 116, 10, 32, 32, 32, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 99, 111, 109, 112, 105, 108, 101, 40, 112, 114, 111, 103, 44, 32, 108, 105, 98, 84, 101, 120, 116, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 111, 117, 116, 102, 105, 108, 101, 32, 61, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 115, 111, 117, 114, 99, 101, 58, 32, 34, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 87, 114, 105, 116, 101, 58, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 40, 115, 41, 32, 123, 32, 116, 104, 105, 115, 46, 115, 111, 117, 114, 99, 101, 32, 43, 61, 32, 115, 59, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 87, 114, 105, 116, 101, 76, 105, 110, 101, 58, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 40, 115, 41, 32, 123, 32, 116, 104, 105, 115, 46, 115, 111, 117, 114, 99, 101, 32, 43, 61, 32, 115, 32, 43, 32, 34, 92, 114, 34, 59, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 32, 61, 32, 91, 93, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 99, 111, 109, 112, 105, 108, 101, 114, 61, 110, 101, 119, 32, 84, 111, 111, 108, 115, 46, 84, 121, 112, 101, 83, 99, 114, 105, 112, 116, 67, 111, 109, 112, 105, 108, 101, 114, 40, 111, 117, 116, 102, 105, 108, 101, 44, 116, 114, 117, 101, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 114, 46, 115, 101, 116, 69, 114, 114, 111, 114, 67, 97, 108, 108, 98, 97, 99, 107, 40, 102, 117, 110, 99, 116, 105, 111, 110, 40, 115, 116, 97, 114, 116, 44, 108, 101, 110, 44, 32, 109, 101, 115, 115, 97, 103, 101, 41, 32, 123, 32, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 46, 112, 117, 115, 104, 40, 123, 115, 116, 97, 114, 116, 58, 115, 116, 97, 114, 116, 44, 32, 108, 101, 110, 58, 108, 101, 110, 44, 32, 109, 101, 115, 115, 97, 103, 101, 58, 109, 101, 115, 115, 97, 103, 101, 125, 41, 59, 32, 125, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 114, 46, 97, 100, 100, 85, 110, 105, 116, 40, 108, 105, 98, 84, 101, 120, 116, 44, 34, 108, 105, 98, 46, 116, 115, 34, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 114, 46, 97, 100, 100, 85, 110, 105, 116, 40, 112, 114, 111, 103, 44, 34, 105, 110, 112, 117, 116, 46, 116, 115, 34, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 114, 46, 116, 121, 112, 101, 67, 104, 101, 99, 107, 40, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 114, 46, 101, 109, 105, 116, 40, 41, 59, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 105, 102, 40, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 46, 108, 101, 110, 103, 116, 104, 32, 62, 32, 48, 32, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 47, 47, 116, 104, 114, 111, 119, 32, 110, 101, 119, 32, 69, 114, 114, 111, 114, 40, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 10, 9, 119, 104, 105, 108, 101, 40, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 91, 48, 93, 32, 61, 61, 32, 39, 47, 39, 32, 38, 38, 32, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 91, 49, 93, 32, 61, 61, 32, 39, 47, 39, 32, 38, 38, 32, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 91, 50, 93, 32, 61, 61, 32, 39, 32, 39, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 32, 61, 32, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 46, 115, 108, 105, 99, 101, 40, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 46, 105, 110, 100, 101, 120, 79, 102, 40, 39, 92, 114, 39, 41, 43, 49, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 101, 114, 114, 111, 114, 80, 114, 101, 102, 105, 120, 32, 61, 32, 34, 34, 59, 10, 9, 102, 111, 114, 40, 118, 97, 114, 32, 105, 32, 61, 32, 48, 59, 105, 60, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 46, 108, 101, 110, 103, 116, 104, 59, 105, 43, 43, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 101, 114, 114, 111, 114, 80, 114, 101, 102, 105, 120, 32, 43, 61, 32, 34, 47, 47, 32, 69, 114, 114, 111, 114, 58, 32, 40, 34, 32, 43, 32, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 91, 105, 93, 46, 115, 116, 97, 114, 116, 32, 43, 32, 34, 44, 34, 32, 43, 32, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 91, 105, 93, 46, 108, 101, 110, 32, 43, 32, 34, 41, 32, 34, 32, 43, 32, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 91, 105, 93, 46, 109, 101, 115, 115, 97, 103, 101, 32, 43, 32, 34, 92, 114, 34, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 114, 101, 116, 117, 114, 110, 32, 101, 114, 114, 111, 114, 80, 114, 101, 102, 105, 120, 32, 43, 32, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 59, 10, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 60, 47, 115, 99, 114, 105, 112, 116, 62, 10, 32, 32, 32, 32, 60, 115, 99, 114, 105, 112, 116, 32, 116, 121, 112, 101, 61, 34, 116, 101, 120, 116, 47, 106, 97, 118, 97, 115, 99, 114, 105, 112, 116, 34, 62, 10, 9, 10, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 108, 105, 98, 84, 101, 120, 116, 32, 61, 32, 34, 34, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 36, 46, 103, 101, 116, 40, 34, 46, 46, 47, 99, 111, 109, 112, 105, 108, 101, 114, 47, 108, 105, 98, 46, 116, 115, 34, 44, 32, 102, 117, 110, 99, 116, 105, 111, 110, 40, 110, 101, 119, 76, 105, 98, 84, 101, 120, 116, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 108, 105, 98, 84, 101, 120, 116, 32, 61, 32, 110, 101, 119, 76, 105, 98, 84, 101, 120, 116, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 41, 59, 9, 10, 32, 32, 32, 32, 32, 32, 32, 32, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 47, 47, 32, 101, 120, 101, 99, 117, 116, 101, 32, 116, 104, 101, 32, 106, 97, 118, 97, 115, 99, 114, 105, 112, 116, 32, 105, 110, 32, 116, 104, 101, 32, 99, 111, 109, 112, 105, 108, 101, 100, 79, 117, 116, 112, 117, 116, 32, 112, 97, 110, 101, 10, 32, 32, 32, 32, 32, 32, 32, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 101, 120, 101, 99, 117, 116, 101, 40, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 36, 40, 39, 35, 99, 111, 109, 112, 105, 108, 97, 116, 105, 111, 110, 39, 41, 46, 116, 101, 120, 116, 40, 34, 82, 117, 110, 110, 105, 110, 103, 46, 46, 46, 34, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 116, 120, 116, 32, 61, 32, 36, 40, 39, 35, 99, 111, 109, 112, 105, 108, 101, 100, 79, 117, 116, 112, 117, 116, 39, 41, 46, 118, 97, 108, 40, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 114, 101, 115, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 116, 114, 121, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 114, 101, 116, 32, 61, 32, 101, 118, 97, 108, 40, 116, 120, 116, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 114, 101, 115, 32, 61, 32, 34, 82, 97, 110, 32, 115, 117, 99, 99, 101, 115, 115, 102, 117, 108, 108, 121, 33, 34, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 32, 99, 97, 116, 99, 104, 40, 101, 41, 32, 123, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 114, 101, 115, 32, 61, 32, 34, 69, 120, 99, 101, 112, 116, 105, 111, 110, 32, 116, 104, 114, 111, 119, 110, 58, 32, 34, 32, 43, 32, 101, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 36, 40, 39, 35, 99, 111, 109, 112, 105, 108, 97, 116, 105, 111, 110, 39, 41, 46, 116, 101, 120, 116, 40, 83, 116, 114, 105, 110, 103, 40, 114, 101, 115, 41, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 47, 47, 32, 114, 101, 99, 111, 109, 112, 105, 108, 101, 32, 116, 104, 101, 32, 115, 116, 114, 97, 100, 97, 83, 114, 99, 32, 97, 110, 100, 32, 112, 111, 112, 117, 108, 97, 116, 101, 32, 116, 104, 101, 32, 99, 111, 109, 112, 105, 108, 101, 100, 79, 117, 116, 112, 117, 116, 32, 112, 97, 110, 101, 10, 32, 32, 32, 32, 32, 32, 32, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 115, 114, 99, 85, 112, 100, 97, 116, 101, 100, 40, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 110, 101, 119, 84, 101, 120, 116, 32, 61, 32, 36, 40, 39, 35, 115, 116, 114, 97, 100, 97, 83, 114, 99, 39, 41, 46, 118, 97, 108, 40, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 99, 111, 109, 112, 105, 108, 101, 100, 83, 111, 117, 114, 99, 101, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 116, 114, 121, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 100, 83, 111, 117, 114, 99, 101, 32, 61, 32, 99, 111, 109, 112, 105, 108, 101, 40, 110, 101, 119, 84, 101, 120, 116, 44, 32, 108, 105, 98, 84, 101, 120, 116, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 32, 99, 97, 116, 99, 104, 32, 40, 101, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 100, 83, 111, 117, 114, 99, 101, 32, 61, 32, 34, 47, 47, 80, 97, 114, 115, 101, 32, 101, 114, 114, 111, 114, 34, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 102, 111, 114, 40, 118, 97, 114, 32, 105, 32, 105, 110, 32, 101, 41, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 100, 83, 111, 117, 114, 99, 101, 32, 43, 61, 32, 34, 92, 114, 47, 47, 32, 34, 32, 43, 32, 101, 91, 105, 93, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 36, 40, 39, 35, 99, 111, 109, 112, 105, 108, 101, 100, 79, 117, 116, 112, 117, 116, 39, 41, 46, 118, 97, 108, 40, 99, 111, 109, 112, 105, 108, 101, 100, 83, 111, 117, 114, 99, 101, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 47, 47, 32, 80, 111, 112, 117, 108, 97, 116, 101, 32, 116, 104, 101, 32, 115, 116, 114, 97, 100, 97, 83, 114, 99, 32, 112, 97, 110, 101, 32, 119, 105, 116, 104, 32, 111, 110, 101, 32, 111, 102, 32, 116, 104, 101, 32, 98, 117, 105, 108, 116, 32, 105, 110, 32, 115, 97, 109, 112, 108, 101, 115, 10, 32, 32, 32, 32, 32, 32, 32, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 101, 120, 97, 109, 112, 108, 101, 83, 101, 108, 101, 99, 116, 105, 111, 110, 67, 104, 97, 110, 103, 101, 100, 40, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 101, 120, 97, 109, 112, 108, 101, 115, 32, 61, 32, 100, 111, 99, 117, 109, 101, 110, 116, 46, 103, 101, 116, 69, 108, 101, 109, 101, 110, 116, 66, 121, 73, 100, 40, 39, 101, 120, 97, 109, 112, 108, 101, 115, 39, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 115, 101, 108, 101, 99, 116, 101, 100, 69, 120, 97, 109, 112, 108, 101, 32, 61, 32, 101, 120, 97, 109, 112, 108, 101, 115, 46, 111, 112, 116, 105, 111, 110, 115, 91, 101, 120, 97, 109, 112, 108, 101, 115, 46, 115, 101, 108, 101, 99, 116, 101, 100, 73, 110, 100, 101, 120, 93, 46, 118, 97, 108, 117, 101, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 105, 102, 32, 40, 115, 101, 108, 101, 99, 116, 101, 100, 69, 120, 97, 109, 112, 108, 101, 32, 33, 61, 32, 34, 34, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 36, 46, 103, 101, 116, 40, 39, 101, 120, 97, 109, 112, 108, 101, 115, 47, 39, 32, 43, 32, 115, 101, 108, 101, 99, 116, 101, 100, 69, 120, 97, 109, 112, 108, 101, 44, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 40, 115, 114, 99, 84, 101, 120, 116, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 36, 40, 39, 35, 115, 116, 114, 97, 100, 97, 83, 114, 99, 39, 41, 46, 118, 97, 108, 40, 115, 114, 99, 84, 101, 120, 116, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 115, 101, 116, 84, 105, 109, 101, 111, 117, 116, 40, 115, 114, 99, 85, 112, 100, 97, 116, 101, 100, 44, 49, 48, 48, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 44, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 40, 101, 114, 114, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 110, 115, 111, 108, 101, 46, 108, 111, 103, 40, 101, 114, 114, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 10, 32, 32, 32, 32, 60, 47, 115, 99, 114, 105, 112, 116, 62, 10, 60, 47, 104, 101, 97, 100, 62, 10, 60, 98, 111, 100, 121, 62, 10, 32, 32, 32, 32, 60, 104, 49, 62, 84, 121, 112, 101, 83, 99, 114, 105, 112, 116, 60, 47, 104, 49, 62, 10, 32, 32, 32, 32, 60, 98, 114, 32, 47, 62, 10, 32, 32, 32, 32, 60, 115, 101, 108, 101, 99, 116, 32, 105, 100, 61, 34, 101, 120, 97, 109, 112, 108, 101, 115, 34, 32, 111, 110, 99, 104, 97, 110, 103, 101, 61, 39, 101, 120, 97, 109, 112, 108, 101, 83, 101, 108, 101, 99, 116, 105, 111, 110, 67, 104, 97, 110, 103, 101, 100, 40, 41, 39, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 111, 112, 116, 105, 111, 110, 32, 118, 97, 108, 117, 101, 61, 34, 34, 62, 83, 101, 108, 101, 99, 116, 46, 46, 46, 60, 47, 111, 112, 116, 105, 111, 110, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 111, 112, 116, 105, 111, 110, 32, 118, 97, 108, 117, 101, 61, 34, 115, 109, 97, 108, 108, 46, 116, 115, 34, 62, 83, 109, 97, 108, 108, 60, 47, 111, 112, 116, 105, 111, 110, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 111, 112, 116, 105, 111, 110, 32, 118, 97, 108, 117, 101, 61, 34, 101, 109, 112, 108, 111, 121, 101, 101, 46, 116, 115, 34, 62, 69, 109, 112, 108, 111, 121, 101, 101, 115, 60, 47, 111, 112, 116, 105, 111, 110, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 111, 112, 116, 105, 111, 110, 32, 118, 97, 108, 117, 101, 61, 34, 99, 111, 110, 119, 97, 121, 46, 116, 115, 34, 62, 67, 111, 110, 119, 97, 121, 32, 71, 97, 109, 101, 32, 111, 102, 32, 76, 105, 102, 101, 60, 47, 111, 112, 116, 105, 111, 110, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 111, 112, 116, 105, 111, 110, 32, 118, 97, 108, 117, 101, 61, 34, 116, 121, 112, 101, 115, 99, 114, 105, 112, 116, 46, 116, 115, 34, 62, 84, 121, 112, 101, 83, 99, 114, 105, 112, 116, 32, 67, 111, 109, 112, 105, 108, 101, 114, 60, 47, 111, 112, 116, 105, 111, 110, 62, 10, 32, 32, 32, 32, 60, 47, 115, 101, 108, 101, 99, 116, 62, 10, 10, 32, 32, 32, 32, 60, 100, 105, 118, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 101, 120, 116, 97, 114, 101, 97, 32, 105, 100, 61, 39, 115, 116, 114, 97, 100, 97, 83, 114, 99, 39, 32, 114, 111, 119, 115, 61, 39, 52, 48, 39, 32, 99, 111, 108, 115, 61, 39, 56, 48, 39, 32, 111, 110, 99, 104, 97, 110, 103, 101, 61, 39, 115, 114, 99, 85, 112, 100, 97, 116, 101, 100, 40, 41, 39, 32, 111, 110, 107, 101, 121, 117, 112, 61, 39, 115, 114, 99, 85, 112, 100, 97, 116, 101, 100, 40, 41, 39, 32, 115, 112, 101, 108, 108, 99, 104, 101, 99, 107, 61, 34, 102, 97, 108, 115, 101, 34, 62, 10, 47, 47, 84, 121, 112, 101, 32, 121, 111, 117, 114, 32, 84, 121, 112, 101, 83, 99, 114, 105, 112, 116, 32, 104, 101, 114, 101, 46, 46, 46, 10, 32, 32, 32, 32, 32, 32, 60, 47, 116, 101, 120, 116, 97, 114, 101, 97, 62, 10, 32, 32, 32, 32, 32, 32, 60, 116, 101, 120, 116, 97, 114, 101, 97, 32, 105, 100, 61, 39, 99, 111, 109, 112, 105, 108, 101, 100, 79, 117, 116, 112, 117, 116, 39, 32, 114, 111, 119, 115, 61, 39, 52, 48, 39, 32, 99, 111, 108, 115, 61, 39, 56, 48, 39, 32, 115, 112, 101, 108, 108, 99, 104, 101, 99, 107, 61, 34, 102, 97, 108, 115, 101, 34, 62, 10, 47, 47, 67, 111, 109, 112, 105, 108, 101, 100, 32, 99, 111, 100, 101, 32, 119, 105, 108, 108, 32, 115, 104, 111, 119, 32, 117, 112, 32, 104, 101, 114, 101, 46, 46, 46, 10, 32, 32, 32, 32, 32, 32, 60, 47, 116, 101, 120, 116, 97, 114, 101, 97, 62, 10, 32, 32, 32, 32, 32, 32, 60, 98, 114, 32, 47, 62, 10, 32, 32, 32, 32, 32, 32, 60, 98, 117, 116, 116, 111, 110, 32, 111, 110, 99, 108, 105, 99, 107, 61, 39, 101, 120, 101, 99, 117, 116, 101, 40, 41, 39, 47, 62, 82, 117, 110, 60, 47, 98, 117, 116, 116, 111, 110, 62, 32, 10, 32, 32, 32, 32, 32, 32, 60, 100, 105, 118, 32, 105, 100, 61, 39, 99, 111, 109, 112, 105, 108, 97, 116, 105, 111, 110, 39, 62, 80, 114, 101, 115, 115, 32, 39, 114, 117, 110, 39, 32, 116, 111, 32, 101, 120, 101, 99, 117, 116, 101, 32, 99, 111, 100, 101, 46, 46, 46, 60, 47, 100, 105, 118, 62, 10, 32, 32, 32, 32, 32, 32, 60, 100, 105, 118, 32, 105, 100, 61, 39, 114, 101, 115, 117, 108, 116, 115, 39, 62, 46, 46, 46, 119, 114, 105, 116, 101, 32, 121, 111, 117, 114, 32, 114, 101, 115, 117, 108, 116, 115, 32, 105, 110, 116, 111, 32, 35, 114, 101, 115, 117, 108, 116, 115, 46, 46, 46, 60, 47, 100, 105, 118, 62, 10, 32, 32, 32, 32, 60, 47, 100, 105, 118, 62, 10, 32, 32, 32, 32, 60, 100, 105, 118, 32, 105, 100, 61, 39, 98, 111, 100, 39, 32, 115, 116, 121, 108, 101, 61, 39, 100, 105, 115, 112, 108, 97, 121, 58, 110, 111, 110, 101, 39, 62, 60, 47, 100, 105, 118, 62, 10, 60, 47, 98, 111, 100, 121, 62, 10, 60, 47, 104, 116, 109, 108, 62, 10 ]); +// This file is determined to be Windows-1252 unless candidateDetectEncoding is set +fixtures['some.shiftjis.1.txt'] = Uint8Array.from([82, 177, 82, 241, 82, 201, 82, 191, 82, 205]); + const lorem = getLorem(); // needle encoded from 'ÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑ' @@ -418,4 +421,3 @@ Donec vehicula mauris eget lacus mollis venenatis et sed nibh. Nam sodales ligul tail: VSBuffer.fromString(`Vivamus iaculis, lacus efficitur faucibus porta, dui nulla facilisis ligula, ut sodales odio nunc id sapien. Cras viverra auctor ipsum, dapibus mattis neque dictum sed. Sed convallis fermentum molestie. Nulla facilisi turpis duis.`) }; } - diff --git a/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts index c01ba24353e..47aa789ae2a 100644 --- a/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ITextFileService, snapshotToString, TextFileOperationError, TextFileOperationResult, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles'; import { URI } from 'vs/base/common/uri'; import { join, basename } from 'vs/base/common/path'; @@ -592,6 +592,21 @@ export default function createSuite(params: Params) { assert.strictEqual(result.encoding, 'windows1252'); }); + test('readStream - autoguessEncoding (candidateGuessEncodings)', async () => { + // This file is determined to be Windows-1252 unless candidateDetectEncoding is set. + const resource = URI.file(join(testDir, 'some.shiftjis.1.txt')); + + const result = await service.readStream(resource, { autoGuessEncoding: true, candidateGuessEncodings: ['utf-8', 'shiftjis', 'euc-jp'] }); + assert.strictEqual(result.encoding, 'shiftjis'); + }); + + test('readStream - autoguessEncoding (candidateGuessEncodings is Empty)', async () => { + const resource = URI.file(join(testDir, 'some_cp1252.txt')); + + const result = await service.readStream(resource, { autoGuessEncoding: true, candidateGuessEncodings: [] }); + assert.strictEqual(result.encoding, 'windows1252'); + }); + test('readStream - FILE_IS_BINARY', async () => { const resource = URI.file(join(testDir, 'binary.txt')); diff --git a/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.test.ts b/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.test.ts index a48728e4a7a..a767ca64fba 100644 --- a/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; diff --git a/src/vs/workbench/services/textfile/test/node/encoding/encoding.integrationTest.ts b/src/vs/workbench/services/textfile/test/node/encoding/encoding.integrationTest.ts index b7da6009727..ebaeb4ab36c 100644 --- a/src/vs/workbench/services/textfile/test/node/encoding/encoding.integrationTest.ts +++ b/src/vs/workbench/services/textfile/test/node/encoding/encoding.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as terminalEncoding from 'vs/base/node/terminalEncoding'; import * as encoding from 'vs/workbench/services/textfile/common/encoding'; diff --git a/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts b/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts index f2a811f9807..6806b4e1c56 100644 --- a/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts +++ b/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import * as encoding from 'vs/workbench/services/textfile/common/encoding'; import * as streams from 'vs/base/common/stream'; @@ -208,6 +208,14 @@ suite('Encoding', () => { assert.strictEqual(mimes.encoding, 'windows1252'); }); + test('autoGuessEncoding (candidateGuessEncodings - ShiftJIS)', async function () { + // This file is determined to be windows1252 unless candidateDetectEncoding is set. + const file = FileAccess.asFileUri('vs/workbench/services/textfile/test/node/encoding/fixtures/some.shiftjis.1.txt').fsPath; + const buffer = await readExactlyByFile(file, 512 * 8); + const mimes = await encoding.detectEncodingFromBuffer(buffer, true, ['utf8', 'shiftjis', 'eucjp']); + assert.strictEqual(mimes.encoding, 'shiftjis'); + }); + async function readAndDecodeFromDisk(path: string, fileEncoding: string | null) { return new Promise((resolve, reject) => { fs.readFile(path, (err, data) => { @@ -246,7 +254,7 @@ suite('Encoding', () => { Buffer.from([65, 66, 67]), ]); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); assert.ok(detected); assert.ok(stream); @@ -262,7 +270,7 @@ suite('Encoding', () => { Buffer.from([65, 66, 67]), ]); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 64, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 64, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); assert.ok(detected); assert.ok(stream); @@ -275,7 +283,7 @@ suite('Encoding', () => { const source = newWriteableBufferStream(); source.end(); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 512, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 512, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); assert.ok(detected); assert.ok(stream); @@ -288,7 +296,7 @@ suite('Encoding', () => { const path = FileAccess.asFileUri('vs/workbench/services/textfile/test/node/encoding/fixtures/some_utf16be.css').fsPath; const source = streamToBufferReadableStream(fs.createReadStream(path)); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 64, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 64, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); assert.strictEqual(detected.encoding, 'utf16be'); assert.strictEqual(detected.seemsBinary, false); @@ -301,7 +309,7 @@ suite('Encoding', () => { test('toDecodeStream - empty file', async function () { const path = FileAccess.asFileUri('vs/workbench/services/textfile/test/node/encoding/fixtures/empty.txt').fsPath; const source = streamToBufferReadableStream(fs.createReadStream(path)); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); const expected = await readAndDecodeFromDisk(path, detected.encoding); const actual = await readAllAsString(stream); @@ -318,7 +326,7 @@ suite('Encoding', () => { } const source = newTestReadableStream(buffers); - const { stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); const expected = new TextDecoder().decode(incompleteEmojis); const actual = await readAllAsString(stream); @@ -330,7 +338,7 @@ suite('Encoding', () => { const path = FileAccess.asFileUri('vs/workbench/services/textfile/test/node/encoding/fixtures/some_gbk.txt').fsPath; const source = streamToBufferReadableStream(fs.createReadStream(path)); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, overwriteEncoding: async () => 'gbk' }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async () => 'gbk' }); assert.ok(detected); assert.ok(stream); @@ -342,7 +350,7 @@ suite('Encoding', () => { const path = FileAccess.asFileUri('vs/workbench/services/textfile/test/node/encoding/fixtures/issue_102202.txt').fsPath; const source = streamToBufferReadableStream(fs.createReadStream(path)); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, overwriteEncoding: async () => 'utf-8' }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async () => 'utf-8' }); assert.ok(detected); assert.ok(stream); @@ -365,7 +373,7 @@ suite('Encoding', () => { let error: Error | undefined = undefined; try { - await encoding.toDecodeStream(source(), { acceptTextOnly: true, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + await encoding.toDecodeStream(source(), { acceptTextOnly: true, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); } catch (e) { error = e; } @@ -375,7 +383,7 @@ suite('Encoding', () => { // acceptTextOnly: false - const { detected, stream } = await encoding.toDecodeStream(source(), { acceptTextOnly: false, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { detected, stream } = await encoding.toDecodeStream(source(), { acceptTextOnly: false, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); assert.ok(detected); assert.strictEqual(detected.seemsBinary, true); diff --git a/src/vs/workbench/services/textfile/test/node/encoding/fixtures/some.shiftjis.1.txt b/src/vs/workbench/services/textfile/test/node/encoding/fixtures/some.shiftjis.1.txt new file mode 100644 index 00000000000..67acab8795e --- /dev/null +++ b/src/vs/workbench/services/textfile/test/node/encoding/fixtures/some.shiftjis.1.txt @@ -0,0 +1,2 @@ +// This file is determined to be Windows-1252 unless candidateDetectEncoding is set. +‚±‚ñ‚É‚¿‚Í \ No newline at end of file diff --git a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts index f38d12fc2bb..34d9c854314 100644 --- a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ITextModel } from 'vs/editor/common/model'; import { URI } from 'vs/base/common/uri'; import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index ec8f4a607cd..ae2c1c6cd0b 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -465,6 +465,6 @@ function handleParentFolder(key: string, selectors: string[]): string { } function escapeCSS(str: string) { - str = str.replace(/[\11\12\14\15\40]/g, '/'); // HTML class names can not contain certain whitespace characters, use / instead, which doesn't exist in file names. + str = str.replace(/[\x11\x12\x14\x15\x40]/g, '/'); // HTML class names can not contain certain whitespace characters, use / instead, which doesn't exist in file names. return mainWindow.CSS.escape(str); } diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 66cfa488890..595e37064c6 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -136,6 +136,8 @@ export class WorkbenchThemeService extends Disposable implements IWorkbenchTheme this.currentProductIconTheme = ProductIconThemeData.createUnloadedTheme(''); this.productIconThemeSequencer = new Sequencer(); + this._register(this.onDidColorThemeChange(theme => getColorRegistry().notifyThemeUpdate(theme))); + // In order to avoid paint flashing for tokens, because // themes are loaded asynchronously, we need to initialize // a color theme document with good defaults until the theme is loaded diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index dd7a8f0d03f..e74a4ab1e05 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -11,7 +11,7 @@ import { convertSettings } from 'vs/workbench/services/themes/common/themeCompat import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import * as resources from 'vs/base/common/resources'; -import { Extensions as ColorRegistryExtensions, IColorRegistry, ColorIdentifier, editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { Extensions as ColorRegistryExtensions, IColorRegistry, ColorIdentifier, editorBackground, editorForeground, DEFAULT_COLOR_CONFIG_VALUE } from 'vs/platform/theme/common/colorRegistry'; import { ITokenStyle, getThemeTypeSelector } from 'vs/platform/theme/common/themeService'; import { Registry } from 'vs/platform/registry/common/platform'; import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; @@ -45,6 +45,10 @@ export type TokenStyleDefinitions = { [P in keyof TokenStyleData]?: TokenStyleDe export type TextMateThemingRuleDefinitions = { [P in keyof TokenStyleData]?: ITextMateThemingRule | undefined; } & { scope?: ProbeScope }; +interface IColorOrDefaultMap { + [id: string]: Color | typeof DEFAULT_COLOR_CONFIG_VALUE; +} + export class ColorThemeData implements IWorkbenchColorTheme { static readonly STORAGE_KEY = 'colorThemeData'; @@ -65,7 +69,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { private themeTokenColors: ITextMateThemingRule[] = []; private customTokenColors: ITextMateThemingRule[] = []; private colorMap: IColorMap = {}; - private customColorMap: IColorMap = {}; + private customColorMap: IColorOrDefaultMap = {}; private semanticTokenRules: SemanticTokenRule[] = []; private customSemanticTokenRules: SemanticTokenRule[] = []; @@ -132,15 +136,20 @@ export class ColorThemeData implements IWorkbenchColorTheme { } public getColor(colorId: ColorIdentifier, useDefault?: boolean): Color | undefined { - let color: Color | undefined = this.customColorMap[colorId]; - if (color) { - return color; + const customColor = this.customColorMap[colorId]; + if (customColor instanceof Color) { + return customColor; + } + if (customColor === undefined) { /* !== DEFAULT_COLOR_CONFIG_VALUE */ + const color = this.colorMap[colorId]; + if (color !== undefined) { + return color; + } } - color = this.colorMap[colorId]; - if (useDefault !== false && types.isUndefined(color)) { - color = this.getDefault(colorId); + if (useDefault !== false) { + return this.getDefault(colorId); } - return color; + return undefined; } private getTokenStyle(type: string, modifiers: string[], language: string, useDefault = true, definitions: TokenStyleDefinitions = {}): TokenStyle | undefined { @@ -346,7 +355,11 @@ export class ColorThemeData implements IWorkbenchColorTheme { } public defines(colorId: ColorIdentifier): boolean { - return this.customColorMap.hasOwnProperty(colorId) || this.colorMap.hasOwnProperty(colorId); + const customColor = this.customColorMap[colorId]; + if (customColor instanceof Color) { + return true; + } + return customColor === undefined /* !== DEFAULT_COLOR_CONFIG_VALUE */ && this.colorMap.hasOwnProperty(colorId); } public setCustomizations(settings: ThemeConfiguration) { @@ -372,7 +385,9 @@ export class ColorThemeData implements IWorkbenchColorTheme { private overwriteCustomColors(colors: IColorCustomizations) { for (const id in colors) { const colorVal = colors[id]; - if (typeof colorVal === 'string') { + if (colorVal === DEFAULT_COLOR_CONFIG_VALUE) { + this.customColorMap[id] = DEFAULT_COLOR_CONFIG_VALUE; + } else if (typeof colorVal === 'string') { this.customColorMap[id] = Color.fromHex(colorVal); } } @@ -716,8 +731,10 @@ async function _loadColorTheme(extensionResourceLoaderService: IExtensionResourc } // new JSON color themes format for (const colorId in colors) { - const colorHex = colors[colorId]; - if (typeof colorHex === 'string') { // ignore colors tht are null + const colorVal = colors[colorId]; + if (colorVal === DEFAULT_COLOR_CONFIG_VALUE) { // ignore colors that are set to to default + delete result.colors[colorId]; + } else if (typeof colorVal === 'string') { result.colors[colorId] = Color.fromHex(colors[colorId]); } } diff --git a/src/vs/workbench/services/themes/test/node/tokenStyleResolving.test.ts b/src/vs/workbench/services/themes/test/node/tokenStyleResolving.test.ts index 98701b3dada..3376ac6e47d 100644 --- a/src/vs/workbench/services/themes/test/node/tokenStyleResolving.test.ts +++ b/src/vs/workbench/services/themes/test/node/tokenStyleResolving.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ColorThemeData } from 'vs/workbench/services/themes/common/colorThemeData'; -import * as assert from 'assert'; +import assert from 'assert'; import { ITokenColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { TokenStyle, getTokenClassificationRegistry } from 'vs/platform/theme/common/tokenClassificationRegistry'; import { Color } from 'vs/base/common/color'; diff --git a/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts b/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts index 17948851797..794fb8960ce 100644 --- a/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts +++ b/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts @@ -68,11 +68,11 @@ export class TunnelService extends AbstractTunnelService { super(logService, configurationService); // Destroy any shared process tunnels that might still be active - lifecycleService.onDidShutdown(() => { + this._register(lifecycleService.onDidShutdown(() => { this._activeSharedProcessTunnels.forEach((id) => { this._sharedProcessTunnelService.destroyTunnel(id); }); - }); + })); } public isPortPrivileged(port: number): boolean { diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.integrationTest.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.integrationTest.ts index f64d8c7e4c8..35936c00f50 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.integrationTest.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index 9ccf8fa049a..1ccafa64c81 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/path'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts b/src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts index f97c4ab35ba..94c72c799df 100644 --- a/src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts +++ b/src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts @@ -7,7 +7,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { DomActivityTracker } from 'vs/workbench/services/userActivity/browser/domActivityTracker'; import { UserActivityService } from 'vs/workbench/services/userActivity/common/userActivityService'; import * as sinon from 'sinon'; -import * as assert from 'assert'; +import assert from 'assert'; suite('DomActivityTracker', () => { let uas: UserActivityService; diff --git a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts index 06c54a5729c..6173c8498f8 100644 --- a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts +++ b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; @@ -114,7 +115,7 @@ export class ExtensionsResource implements IProfileResource { return JSON.stringify(exclude?.length ? extensions.filter(e => !exclude.includes(e.identifier.id.toLowerCase())) : extensions); } - async apply(content: string, profile: IUserDataProfile): Promise { + async apply(content: string, profile: IUserDataProfile, progress?: (message: string) => void, token?: CancellationToken): Promise { return this.withProfileScopedServices(profile, async (extensionEnablementService) => { const profileExtensions: IProfileExtension[] = await this.getProfileExtensions(content); const installedExtensions = await this.extensionManagementService.getInstalled(undefined, profile.extensionsResource); @@ -168,7 +169,17 @@ export class ExtensionsResource implements IProfileResource { } })); if (installExtensionInfos.length) { - await this.extensionManagementService.installGalleryExtensions(installExtensionInfos); + if (token) { + for (const installExtensionInfo of installExtensionInfos) { + if (token.isCancellationRequested) { + return; + } + progress?.(localize('installingExtension', "Installing extension {0}...", installExtensionInfo.extension.displayName ?? installExtensionInfo.extension.identifier.id)); + await this.extensionManagementService.installFromGallery(installExtensionInfo.extension, installExtensionInfo.options); + } + } else { + await this.extensionManagementService.installGalleryExtensions(installExtensionInfos); + } } this.logService.info(`Importing Profile (${profile.name}): Finished installing extensions.`); } @@ -284,6 +295,7 @@ export abstract class ExtensionsResourceTreeItem implements IProfileResourceTree label: localize('exclude', "Select {0} Extension", e.displayName || e.identifier.id), } } : undefined, + themeIcon: Codicon.extensions, command: { id: 'extension.open', title: '', diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts index 13b592b7e0e..6a28cadf448 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts @@ -24,7 +24,6 @@ import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/con import { Registry } from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { ILogService } from 'vs/platform/log/common/log'; import { TreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { SettingsResource, SettingsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/settingsResource'; import { KeybindingsResource, KeybindingsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/keybindingsResource'; @@ -64,7 +63,7 @@ import { Action, ActionRunner, IAction, IActionRunner } from 'vs/base/common/act import { isWeb } from 'vs/base/common/platform'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { Codicon, getAllCodicons } from 'vs/base/common/codicons'; -import { Barrier } from 'vs/base/common/async'; +import { Barrier, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -81,6 +80,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import type { IHoverWidget } from 'vs/base/browser/ui/hover/hover'; import { IAccessibleViewInformationService } from 'vs/workbench/services/accessibility/common/accessibleViewInformationService'; +import { ILogService } from 'vs/platform/log/common/log'; interface IUserDataProfileTemplate { readonly name: string; @@ -193,7 +193,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU this.profileContentHandlers.delete(id); } - async exportProfile(): Promise { + async exportProfile2(): Promise { if (this.isProfileExportInProgressContextKey.get()) { this.logService.warn('Profile export already in progress.'); return; @@ -225,7 +225,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU if (mode === 'preview') { await this.previewProfile(profileTemplate, options); } else if (mode === 'apply') { - await this.createAndSwitch(profileTemplate, false, true, options, localize('create profile', "Create Profile")); + await this.createAndSwitch(profileTemplate, !!options?.transient, true, options, localize('create profile', "Create Profile")); } else if (mode === 'both') { await this.importAndPreviewProfile(uri, profileTemplate, options); } @@ -242,6 +242,123 @@ export class UserDataProfileImportExportService extends Disposable implements IU return this.saveProfile(profile); } + async createFromProfile(from: IUserDataProfile, options: IUserDataProfileCreateOptions, token: CancellationToken): Promise { + const disposables = new DisposableStore(); + let creationPromise: CancelablePromise; + disposables.add(token.onCancellationRequested(() => creationPromise.cancel())); + let profile: IUserDataProfile | undefined; + return this.progressService.withProgress({ + location: ProgressLocation.Notification, + delay: 500, + sticky: true, + cancellable: true, + }, async progress => { + const reportProgress = (message: string) => progress.report({ message: localize('create from profile', "Create Profile: {0}", message) }); + creationPromise = createCancelablePromise(async token => { + const userDataProfilesExportState = disposables.add(this.instantiationService.createInstance(UserDataProfileExportState, from, { ...options?.resourceTypeFlags, extensions: false })); + const profileTemplate = await userDataProfilesExportState.getProfileTemplate(options.name ?? from.name, options?.icon); + profile = await this.getProfileToImport({ ...profileTemplate, name: options.name ?? profileTemplate.name }, !!options.transient, options); + if (!profile) { + return; + } + if (token.isCancellationRequested) { + return; + } + await this.applyProfileTemplate(profileTemplate, profile, options, reportProgress, token); + }); + try { + await creationPromise; + if (profile && (options?.resourceTypeFlags?.extensions ?? true)) { + reportProgress(localize('installing extensions', "Installing Extensions...")); + await this.instantiationService.createInstance(ExtensionsResource).copy(from, profile, false); + } + } catch (error) { + if (profile) { + await this.userDataProfilesService.removeProfile(profile); + profile = undefined; + } + } + return profile; + + }, () => creationPromise.cancel()).finally(() => disposables.dispose()); + } + + async createProfileFromTemplate(profileTemplate: IUserDataProfileTemplate, options: IUserDataProfileCreateOptions, token: CancellationToken): Promise { + const disposables = new DisposableStore(); + let creationPromise: CancelablePromise; + disposables.add(token.onCancellationRequested(() => creationPromise.cancel())); + let profile: IUserDataProfile | undefined; + return this.progressService.withProgress({ + location: ProgressLocation.Notification, + delay: 500, + sticky: true, + cancellable: true, + }, async progress => { + const reportProgress = (message: string) => progress.report({ message: localize('create from profile', "Create Profile: {0}", message) }); + creationPromise = createCancelablePromise(async token => { + profile = await this.getProfileToImport({ ...profileTemplate, name: options.name ?? profileTemplate.name }, !!options.transient, options); + if (!profile) { + return; + } + if (token.isCancellationRequested) { + return; + } + await this.applyProfileTemplate(profileTemplate, profile, options, reportProgress, token); + }); + try { + await creationPromise; + } catch (error) { + if (profile) { + await this.userDataProfilesService.removeProfile(profile); + profile = undefined; + } + } + return profile; + }, () => creationPromise.cancel()).finally(() => disposables.dispose()); + } + + private async applyProfileTemplate(profileTemplate: IUserDataProfileTemplate, profile: IUserDataProfile, options: IUserDataProfileCreateOptions, reportProgress: (message: string) => void, token: CancellationToken): Promise { + if (profileTemplate.settings && (options.resourceTypeFlags?.settings ?? true) && !profile.useDefaultFlags?.settings) { + reportProgress(localize('creating settings', "Creating Settings...")); + await this.instantiationService.createInstance(SettingsResource).apply(profileTemplate.settings, profile); + } + if (token.isCancellationRequested) { + return; + } + if (profileTemplate.keybindings && (options.resourceTypeFlags?.keybindings ?? true) && !profile.useDefaultFlags?.keybindings) { + reportProgress(localize('create keybindings', "Creating Keyboard Shortcuts...")); + await this.instantiationService.createInstance(KeybindingsResource).apply(profileTemplate.keybindings, profile); + } + if (token.isCancellationRequested) { + return; + } + if (profileTemplate.tasks && (options.resourceTypeFlags?.tasks ?? true) && !profile.useDefaultFlags?.tasks) { + reportProgress(localize('create tasks', "Creating Tasks...")); + await this.instantiationService.createInstance(TasksResource).apply(profileTemplate.tasks, profile); + } + if (token.isCancellationRequested) { + return; + } + if (profileTemplate.snippets && (options.resourceTypeFlags?.snippets ?? true) && !profile.useDefaultFlags?.snippets) { + reportProgress(localize('create snippets', "Creating Snippets...")); + await this.instantiationService.createInstance(SnippetsResource).apply(profileTemplate.snippets, profile); + } + if (token.isCancellationRequested) { + return; + } + if (profileTemplate.globalState && !profile.useDefaultFlags?.globalState) { + reportProgress(localize('applying global state', "Applying UI State...")); + await this.instantiationService.createInstance(GlobalStateResource).apply(profileTemplate.globalState, profile); + } + if (token.isCancellationRequested) { + return; + } + if (profileTemplate.extensions && (options.resourceTypeFlags?.extensions ?? true) && !profile.useDefaultFlags?.extensions) { + reportProgress(localize('installing extensions', "Installing Extensions...")); + await this.instantiationService.createInstance(ExtensionsResource).apply(profileTemplate.extensions, profile, reportProgress, token); + } + } + private saveProfile(profile: IUserDataProfile): Promise; private saveProfile(profile?: IUserDataProfile, source?: IUserDataProfile | URI | IUserDataProfileTemplate): Promise; private async saveProfile(profile?: IUserDataProfile, source?: IUserDataProfile | URI | Mutable): Promise { @@ -527,7 +644,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU await this.importProfile(source, { mode: 'apply', name: result.name, useDefaultFlags, icon: result.icon ? result.icon : undefined }); } else if (isUserDataProfile(source)) { this.telemetryService.publicLog2('userDataProfile.createFromProfile', createProfileTelemetryData); - await this.createFromProfile(source, result.name, { useDefaultFlags, icon: result.icon ? result.icon : undefined }); + await this._createFromProfile(source, result.name, { useDefaultFlags, icon: result.icon ? result.icon : undefined }); } else if (isUserDataProfileTemplate(source)) { source.name = result.name; this.telemetryService.publicLog2('userDataProfile.createFromExternalTemplate', createProfileTelemetryData); @@ -572,7 +689,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - async exportProfile2(profile: IUserDataProfile): Promise { + async exportProfile(profile: IUserDataProfile): Promise { const disposables = new DisposableStore(); try { const userDataProfilesExportState = disposables.add(this.instantiationService.createInstance(UserDataProfileExportState, profile, undefined)); @@ -582,7 +699,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - async createFromProfile(profile: IUserDataProfile, name: string, options?: IUserDataProfileCreateOptions): Promise { + private async _createFromProfile(profile: IUserDataProfile, name: string, options?: IUserDataProfileCreateOptions): Promise { const userDataProfilesExportState = this.instantiationService.createInstance(UserDataProfileExportState, profile, options?.resourceTypeFlags); try { const profileTemplate = await userDataProfilesExportState.getProfileTemplate(name, options?.icon); @@ -592,13 +709,12 @@ export class UserDataProfileImportExportService extends Disposable implements IU sticky: true, }, async progress => { const reportProgress = (message: string) => progress.report({ message: localize('create from profile', "Create Profile: {0}", message) }); - const createdProfile = await this.doCreateProfile(profileTemplate, false, false, { useDefaultFlags: options?.useDefaultFlags, icon: options?.icon }, reportProgress); + const createdProfile = await this.doCreateProfile(profileTemplate, false, false, { useDefaultFlags: options?.useDefaultFlags, icon: options?.icon, transient: options?.transient }, reportProgress); if (createdProfile) { if (options?.resourceTypeFlags?.extensions ?? true) { reportProgress(localize('progress extensions', "Applying Extensions...")); await this.instantiationService.createInstance(ExtensionsResource).copy(profile, createdProfile, false); } - reportProgress(localize('switching profile', "Switching Profile...")); await this.userDataProfileManagementService.switchProfile(createdProfile); } @@ -1068,24 +1184,6 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - async setProfile(profile: IUserDataProfileTemplate): Promise { - await this.progressService.withProgress({ - location: ProgressLocation.Notification, - title: localize('profiles.applying', "{0}: Applying...", PROFILES_CATEGORY.value), - }, async progress => { - if (profile.settings) { - await this.instantiationService.createInstance(SettingsResource).apply(profile.settings, this.userDataProfileService.currentProfile); - } - if (profile.globalState) { - await this.instantiationService.createInstance(GlobalStateResource).apply(profile.globalState, this.userDataProfileService.currentProfile); - } - if (profile.extensions) { - await this.instantiationService.createInstance(ExtensionsResource).apply(profile.extensions, this.userDataProfileService.currentProfile); - } - }); - this.notificationService.info(localize('applied profile', "{0}: Applied successfully.", PROFILES_CATEGORY.value)); - } - } class FileUserDataProfileContentHandler implements IUserDataProfileContentHandler { diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts index e454c34038a..ff845818b8d 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts @@ -79,6 +79,10 @@ export class UserDataProfileManagementService extends Disposable implements IUse } } + async createProfile(name: string, options?: IUserDataProfileOptions): Promise { + return this.userDataProfilesService.createNamedProfile(name, options); + } + async createAndEnterProfile(name: string, options?: IUserDataProfileOptions): Promise { const profile = await this.userDataProfilesService.createNamedProfile(name, options, toWorkspaceIdentifier(this.workspaceContextService.getWorkspace())); await this.changeCurrentProfile(profile); @@ -93,15 +97,16 @@ export class UserDataProfileManagementService extends Disposable implements IUse return profile; } - async updateProfile(profile: IUserDataProfile, updateOptions: IUserDataProfileUpdateOptions): Promise { + async updateProfile(profile: IUserDataProfile, updateOptions: IUserDataProfileUpdateOptions): Promise { if (!this.userDataProfilesService.profiles.some(p => p.id === profile.id)) { throw new Error(`Profile ${profile.name} does not exist`); } if (profile.isDefault) { throw new Error(localize('cannotRenameDefaultProfile', "Cannot rename the default profile")); } - await this.userDataProfilesService.updateProfile(profile, updateOptions); + const updatedProfile = await this.userDataProfilesService.updateProfile(profile, updateOptions); this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'updateProfile' }); + return updatedProfile; } async removeProfile(profile: IUserDataProfile): Promise { diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileStorageService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileStorageService.ts index 3dfaaf20e4c..24a18be8598 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileStorageService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileStorageService.ts @@ -24,7 +24,7 @@ export class UserDataProfileStorageService extends AbstractUserDataProfileStorag @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @ILogService private readonly logService: ILogService, ) { - super(storageService); + super(true, storageService); const disposables = this._register(new DisposableStore()); this._register(Event.filter(storageService.onDidChangeTarget, e => e.scope === StorageScope.PROFILE, disposables)(() => this.onDidChangeStorageTargetInCurrentProfile())); this._register(storageService.onDidChangeValue(StorageScope.PROFILE, undefined, disposables)(e => this.onDidChangeStorageValueInCurrentProfile(e))); diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts index d2c5cb4487e..e5c260e0510 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts @@ -42,16 +42,19 @@ export const IUserDataProfileManagementService = createDecorator; createAndEnterProfile(name: string, options?: IUserDataProfileOptions): Promise; createAndEnterTransientProfile(): Promise; removeProfile(profile: IUserDataProfile): Promise; - updateProfile(profile: IUserDataProfile, updateOptions: IUserDataProfileUpdateOptions): Promise; + updateProfile(profile: IUserDataProfile, updateOptions: IUserDataProfileUpdateOptions): Promise; switchProfile(profile: IUserDataProfile): Promise; getBuiltinProfileTemplates(): Promise; } export interface IUserDataProfileTemplate { + readonly name: string; + readonly icon?: string; readonly settings?: string; readonly keybindings?: string; readonly tasks?: string; @@ -78,15 +81,15 @@ export function toUserDataProfileUri(path: string, productService: IProductServi }); } -export interface IProfileImportOptions extends IUserDataProfileOptions { +export interface IUserDataProfileCreateOptions extends IUserDataProfileOptions { readonly name?: string; - readonly icon?: string; - readonly mode?: 'preview' | 'apply' | 'both'; readonly resourceTypeFlags?: ProfileResourceTypeFlags; } -export interface IUserDataProfileCreateOptions extends IUserDataProfileOptions { - readonly resourceTypeFlags?: ProfileResourceTypeFlags; +export interface IProfileImportOptions extends IUserDataProfileCreateOptions { + readonly name?: string; + readonly icon?: string; + readonly mode?: 'preview' | 'apply' | 'both'; } export const IUserDataProfileImportExportService = createDecorator('IUserDataProfileImportExportService'); @@ -97,15 +100,15 @@ export interface IUserDataProfileImportExportService { unregisterProfileContentHandler(id: string): void; resolveProfileTemplate(uri: URI): Promise; - exportProfile(): Promise; - exportProfile2(profile: IUserDataProfile): Promise; + exportProfile(profile: IUserDataProfile): Promise; + exportProfile2(): Promise; importProfile(uri: URI, options?: IProfileImportOptions): Promise; showProfileContents(): Promise; createProfile(from?: IUserDataProfile | URI): Promise; - createFromProfile(from: IUserDataProfile, name: string, options?: IUserDataProfileCreateOptions): Promise; editProfile(profile: IUserDataProfile): Promise; + createFromProfile(from: IUserDataProfile, options: IUserDataProfileCreateOptions, token: CancellationToken): Promise; + createProfileFromTemplate(profileTemplate: IUserDataProfileTemplate, options: IUserDataProfileCreateOptions, token: CancellationToken): Promise; createTroubleshootProfile(): Promise; - setProfile(profile: IUserDataProfileTemplate): Promise; } export interface IProfileResourceInitializer { diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts index 93251b9b1c3..6e0e28a0347 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts @@ -71,7 +71,7 @@ export const ICONS = [ Codicon.pulse, Codicon.radioTower, Codicon.smiley, - Codicon.symbolEvent, + Codicon.zap, Codicon.squirrel, Codicon.symbolColor, Codicon.mail, diff --git a/src/vs/workbench/services/views/browser/viewsService.ts b/src/vs/workbench/services/views/browser/viewsService.ts index 488f960b4ac..70965dab987 100644 --- a/src/vs/workbench/services/views/browser/viewsService.ts +++ b/src/vs/workbench/services/views/browser/viewsService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable, DisposableStore, DisposableMap } from 'vs/base/common/lifecycle'; import { IViewDescriptorService, ViewContainer, IViewDescriptor, IView, ViewContainerLocation, IViewPaneContainer } from 'vs/workbench/common/views'; import { FocusedViewContext, getVisbileViewContextKey } from 'vs/workbench/common/contextkeys'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -51,6 +51,7 @@ export class ViewsService extends Disposable implements IViewsService { private readonly _onDidChangeFocusedView = this._register(new Emitter()); readonly onDidChangeFocusedView = this._onDidChangeFocusedView.event; + private readonly viewContainerDisposables = this._register(new DisposableMap()); private readonly enabledViewContainersContextKeys: Map>; private readonly visibleViewContextKeys: Map>; private readonly focusedViewContextKey: IContextKey; @@ -114,7 +115,7 @@ export class ViewsService extends Disposable implements IViewsService { private onDidChangeContainers(added: ReadonlyArray<{ container: ViewContainer; location: ViewContainerLocation }>, removed: ReadonlyArray<{ container: ViewContainer; location: ViewContainerLocation }>): void { for (const { container, location } of removed) { - this.deregisterPaneComposite(container, location); + this.onDidDeregisterViewContainer(container, location); } for (const { container, location } of added) { this.onDidRegisterViewContainer(container, location); @@ -123,15 +124,24 @@ export class ViewsService extends Disposable implements IViewsService { private onDidRegisterViewContainer(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void { this.registerPaneComposite(viewContainer, viewContainerLocation); + const disposables = new DisposableStore(); + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); this.onViewDescriptorsAdded(viewContainerModel.allViewDescriptors, viewContainer); - this._register(viewContainerModel.onDidChangeAllViewDescriptors(({ added, removed }) => { + disposables.add(viewContainerModel.onDidChangeAllViewDescriptors(({ added, removed }) => { this.onViewDescriptorsAdded(added, viewContainer); this.onViewDescriptorsRemoved(removed); })); this.updateViewContainerEnablementContextKey(viewContainer); - this._register(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.updateViewContainerEnablementContextKey(viewContainer))); - this._register(this.registerOpenViewContainerAction(viewContainer)); + disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.updateViewContainerEnablementContextKey(viewContainer))); + disposables.add(this.registerOpenViewContainerAction(viewContainer)); + + this.viewContainerDisposables.set(viewContainer.id, disposables); + } + + private onDidDeregisterViewContainer(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void { + this.deregisterPaneComposite(viewContainer, viewContainerLocation); + this.viewContainerDisposables.deleteAndDispose(viewContainer.id); } private onDidChangeContainerLocation(viewContainer: ViewContainer, from: ViewContainerLocation, to: ViewContainerLocation): void { diff --git a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts index f37d4af42b7..c20ad890065 100644 --- a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts +++ b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { IViewsRegistry, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewContainerModel, IViewDescriptorService, ViewContainer } from 'vs/workbench/common/views'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts b/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts index 31b8bb8a6a3..f9ab97f41e2 100644 --- a/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts +++ b/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as assert from 'assert'; +import assert from 'assert'; import { IViewsRegistry, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, ViewContainer, ViewContainerLocationToString } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 1a950b3e8dc..35eb7cf3c8e 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -38,6 +38,7 @@ import { Schemas } from 'vs/base/common/network'; import { IDecorationData, IDecorationsProvider, IDecorationsService } from 'vs/workbench/services/decorations/common/decorations'; import { Codicon } from 'vs/base/common/codicons'; import { listErrorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IProgressService } from 'vs/platform/progress/common/progress'; export interface IFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { @@ -162,7 +163,8 @@ export class FileWorkingCopyManager extend @INotificationService private readonly notificationService: INotificationService, @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService, @IEditorService private readonly editorService: IEditorService, - @IElevatedFileService private readonly elevatedFileService: IElevatedFileService + @IElevatedFileService private readonly elevatedFileService: IElevatedFileService, + @IProgressService private readonly progressService: IProgressService ) { super(resource, fileService); @@ -865,7 +867,7 @@ export class StoredFileWorkingCopy extend options.reason = SaveReason.EXPLICIT; } - let versionId = this.versionId; + const versionId = this.versionId; this.trace(`doSave(${versionId}) - enter with versionId ${versionId}`); // Return early if saved from within save participant to break recursion @@ -929,6 +931,21 @@ export class StoredFileWorkingCopy extend const saveCancellation = new CancellationTokenSource(); + return this.progressService.withProgress({ + title: localize('saveParticipants', "Saving '{0}'", this.name), + location: ProgressLocation.Window, + cancellable: true, + delay: this.isDirty() ? 3000 : 5000 + }, progress => { + return this.doSaveSequential(versionId, options, progress, saveCancellation); + }, () => { + saveCancellation.cancel(); + }).finally(() => { + saveCancellation.dispose(); + }); + } + + private doSaveSequential(versionId: number, options: IStoredFileWorkingCopySaveAsOptions, progress: IProgress, saveCancellation: CancellationTokenSource): Promise { return this.saveSequentializer.run(versionId, (async () => { // A save participant can still change the working copy now @@ -964,7 +981,7 @@ export class StoredFileWorkingCopy extend if (!saveCancellation.token.isCancellationRequested) { this.ignoreSaveFromSaveParticipants = true; try { - await this.workingCopyFileService.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT, savedFrom: options.from }, saveCancellation.token); + await this.workingCopyFileService.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT, savedFrom: options.from }, progress, saveCancellation.token); } finally { this.ignoreSaveFromSaveParticipants = false; } @@ -1004,6 +1021,7 @@ export class StoredFileWorkingCopy extend // Save to Disk. We mark the save operation as currently running with // the latest versionId because it might have changed from a save // participant triggering + progress.report({ message: localize('saveTextFile', "Writing into file...") }); this.trace(`doSave(${versionId}) - before write()`); const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); const resolvedFileWorkingCopy = this; diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts index f445e7d36dd..bbd5aa58f01 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts @@ -30,6 +30,7 @@ import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/wo import { isWeb } from 'vs/base/common/platform'; import { onUnexpectedError } from 'vs/base/common/errors'; import { SnapshotContext } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { IProgressService } from 'vs/platform/progress/common/progress'; /** * The only one that should be dealing with `IStoredFileWorkingCopy` and handle all @@ -186,7 +187,8 @@ export class StoredFileWorkingCopyManager @INotificationService private readonly notificationService: INotificationService, @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService, @IEditorService private readonly editorService: IEditorService, - @IElevatedFileService private readonly elevatedFileService: IElevatedFileService + @IElevatedFileService private readonly elevatedFileService: IElevatedFileService, + @IProgressService private readonly progressService: IProgressService ) { super(fileService, logService, workingCopyBackupService); @@ -532,7 +534,7 @@ export class StoredFileWorkingCopyManager async options => { await this.resolve(resource, { ...options, reload: { async: false } }); }, this.fileService, this.logService, this.workingCopyFileService, this.filesConfigurationService, this.workingCopyBackupService, this.workingCopyService, this.notificationService, this.workingCopyEditorService, - this.editorService, this.elevatedFileService + this.editorService, this.elevatedFileService, this.progressService ); workingCopyResolve = workingCopy.resolve(resolveOptions); diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopySaveParticipant.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopySaveParticipant.ts index 2473efad9f0..240beb17907 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopySaveParticipant.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopySaveParticipant.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { raceCancellation } from 'vs/base/common/async'; -import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { insert } from 'vs/base/common/arrays'; import { IStoredFileWorkingCopySaveParticipant, IStoredFileWorkingCopySaveParticipantContext } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; @@ -20,7 +19,6 @@ export class StoredFileWorkingCopySaveParticipant extends Disposable { get length(): number { return this.saveParticipants.length; } constructor( - @IProgressService private readonly progressService: IProgressService, @ILogService private readonly logService: ILogService ) { super(); @@ -32,41 +30,26 @@ export class StoredFileWorkingCopySaveParticipant extends Disposable { return toDisposable(() => remove()); } - participate(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, token: CancellationToken): Promise { - const cts = new CancellationTokenSource(token); + async participate(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { - return this.progressService.withProgress({ - title: localize('saveParticipants', "Saving '{0}'", workingCopy.name), - location: ProgressLocation.Notification, - cancellable: true, - delay: workingCopy.isDirty() ? 3000 : 5000 - }, async progress => { + // undoStop before participation + workingCopy.model?.pushStackElement(); - // undoStop before participation - workingCopy.model?.pushStackElement(); - - for (const saveParticipant of this.saveParticipants) { - if (cts.token.isCancellationRequested || workingCopy.isDisposed()) { - break; - } - - try { - const promise = saveParticipant.participate(workingCopy, context, progress, cts.token); - await raceCancellation(promise, cts.token); - } catch (err) { - this.logService.warn(err); - } + for (const saveParticipant of this.saveParticipants) { + if (token.isCancellationRequested || workingCopy.isDisposed()) { + break; } - // undoStop after participation - workingCopy.model?.pushStackElement(); + try { + const promise = saveParticipant.participate(workingCopy, context, progress, token); + await raceCancellation(promise, token); + } catch (err) { + this.logService.warn(err); + } + } - // Cleanup - cts.dispose(); - }, () => { - // user cancel - cts.dispose(true); - }); + // undoStop after participation + workingCopy.model?.pushStackElement(); } override dispose(): void { diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts index 1896c957597..704be2bbecd 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts @@ -206,7 +206,7 @@ export interface IWorkingCopyFileService { /** * Runs all available save participants for stored file working copies. */ - runSaveParticipants(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, token: CancellationToken): Promise; + runSaveParticipants(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress, token: CancellationToken): Promise; //#endregion @@ -507,8 +507,8 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi return this.saveParticipants.addSaveParticipant(participant); } - runSaveParticipants(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, token: CancellationToken): Promise { - return this.saveParticipants.participate(workingCopy, context, token); + runSaveParticipants(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { + return this.saveParticipants.participate(workingCopy, context, progress, token); } //#endregion diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index 16185733f61..be93a5dea1f 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService, TestServiceAccessor, TestInMemoryFileSystemProvider } from 'vs/workbench/test/browser/workbenchTestServices'; @@ -40,7 +40,7 @@ suite('FileWorkingCopyManager', () => { accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.fileDialogService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.pathService, - accessor.environmentService, accessor.dialogService, accessor.decorationsService + accessor.environmentService, accessor.dialogService, accessor.decorationsService, accessor.progressService, )); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts index fee71d4e7d0..527e47deac1 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { CancellationToken } from 'vs/base/common/cancellation'; diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts index d4d63ff6958..33c986d3432 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory, isStoredFileWorkingCopySaveEvent, IStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; @@ -151,7 +151,7 @@ suite('StoredFileWorkingCopy (with custom save)', function () { accessor = instantiationService.createInstance(TestServiceAccessor); const resource = URI.file('test/resource'); - workingCopy = disposables.add(new StoredFileWorkingCopy('testStoredFileWorkingCopyType', resource, basename(resource), factory, options => workingCopy.resolve(options), accessor.fileService, accessor.logService, accessor.workingCopyFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService)); + workingCopy = disposables.add(new StoredFileWorkingCopy('testStoredFileWorkingCopyType', resource, basename(resource), factory, options => workingCopy.resolve(options), accessor.fileService, accessor.logService, accessor.workingCopyFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.progressService)); }); teardown(() => { @@ -249,7 +249,7 @@ suite('StoredFileWorkingCopy', function () { let workingCopy: StoredFileWorkingCopy; function createWorkingCopy(uri: URI = resource) { - const workingCopy: StoredFileWorkingCopy = new StoredFileWorkingCopy('testStoredFileWorkingCopyType', uri, basename(uri), factory, options => workingCopy.resolve(options), accessor.fileService, accessor.logService, accessor.workingCopyFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService); + const workingCopy: StoredFileWorkingCopy = new StoredFileWorkingCopy('testStoredFileWorkingCopyType', uri, basename(uri), factory, options => workingCopy.resolve(options), accessor.fileService, accessor.logService, accessor.workingCopyFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.progressService); return workingCopy; } diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts index 54366a2df21..08dc69d5ae5 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService, TestServiceAccessor, TestWillShutdownEvent } from 'vs/workbench/test/browser/workbenchTestServices'; @@ -37,7 +37,8 @@ suite('StoredFileWorkingCopyManager', () => { accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService, accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, - accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService + accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, + accessor.progressService )); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts index 42177887afd..5b9239849f7 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBufferReadableStream, newWriteableBufferStream, VSBuffer, streamToBuffer, bufferToStream, readableToBuffer, VSBufferReadable } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index cfac100bd5b..45e86fd3d38 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; @@ -39,7 +39,7 @@ suite('UntitledFileWorkingCopyManager', () => { accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.fileDialogService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.pathService, - accessor.environmentService, accessor.dialogService, accessor.decorationsService + accessor.environmentService, accessor.dialogService, accessor.decorationsService, accessor.progressService )); }); @@ -318,7 +318,7 @@ suite('UntitledFileWorkingCopyManager', () => { accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.fileDialogService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.pathService, - accessor.environmentService, accessor.dialogService, accessor.decorationsService + accessor.environmentService, accessor.dialogService, accessor.decorationsService, accessor.progressService )); const untitled1OriginalType = disposables.add(await manager.untitled.resolve()); @@ -340,7 +340,7 @@ suite('UntitledFileWorkingCopyManager', () => { accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.fileDialogService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.pathService, - accessor.environmentService, accessor.dialogService, accessor.decorationsService + accessor.environmentService, accessor.dialogService, accessor.decorationsService, accessor.progressService )); const result = disposables.add(await manager.untitled.resolve()); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts index a04a7c4951a..91926cb1774 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBufferReadableStream, VSBuffer, streamToBuffer, bufferToStream, readableToBuffer, VSBufferReadable } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts index 1e741e7bc9e..5dff0762979 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts index 9821fae2c4a..e759ceb0d82 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts index af1ee0b5b00..a6458af7ac9 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index 384e3b17955..e0214cd2d79 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { URI } from 'vs/base/common/uri'; import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts index b4ee3100922..2e70ca1e8b4 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; import { insert } from 'vs/base/common/arrays'; import { hash } from 'vs/base/common/hash'; diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts index fa293b361cf..ae14f5d12d4 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts index 40a3f059f9b..83bd0aeebad 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TestContextService, TestStorageService, TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; import { NullLogService } from 'vs/platform/log/common/log'; import { FileService } from 'vs/platform/files/common/fileService'; diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryTracker.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryTracker.test.ts index ef04d7041db..b749708ada9 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryTracker.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { TestContextService, TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; import { randomPath } from 'vs/base/common/extpath'; diff --git a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts index 21312c7e76c..a3be20f5277 100644 --- a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts @@ -29,8 +29,9 @@ import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/w import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { Disposable } from 'vs/base/common/lifecycle'; -export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditingService { +export abstract class AbstractWorkspaceEditingService extends Disposable implements IWorkspaceEditingService { declare readonly _serviceBrand: undefined; @@ -51,7 +52,9 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, - ) { } + ) { + super(); + } async pickNewWorkspacePath(): Promise { const availableFileSystems = [Schemas.file]; diff --git a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts index f0453fbdc65..a896a9ed6d1 100644 --- a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts @@ -67,10 +67,10 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi } private registerListeners(): void { - this.lifecycleService.onBeforeShutdown(e => { + this._register(this.lifecycleService.onBeforeShutdown(e => { const saveOperation = this.saveUntitledBeforeShutdown(e.reason); e.veto(saveOperation, 'veto.untitledWorkspace'); - }); + })); } private async saveUntitledBeforeShutdown(reason: ShutdownReason): Promise { diff --git a/src/vs/workbench/services/workspaces/test/browser/workspaces.test.ts b/src/vs/workbench/services/workspaces/test/browser/workspaces.test.ts index a1e1a9f728f..166b616d4c5 100644 --- a/src/vs/workbench/services/workspaces/test/browser/workspaces.test.ts +++ b/src/vs/workbench/services/workspaces/test/browser/workspaces.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getWorkspaceIdentifier, getSingleFolderWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; diff --git a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts index 6b9bd8a882d..203cde08ee2 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/test/browser/codeeditor.test.ts b/src/vs/workbench/test/browser/codeeditor.test.ts index 686bc17e88b..59ac60e96ff 100644 --- a/src/vs/workbench/test/browser/codeeditor.test.ts +++ b/src/vs/workbench/test/browser/codeeditor.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/test/browser/contributions.test.ts b/src/vs/workbench/test/browser/contributions.test.ts index 8f878d6ab53..adb429673b1 100644 --- a/src/vs/workbench/test/browser/contributions.test.ts +++ b/src/vs/workbench/test/browser/contributions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DeferredPromise } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isCI } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/test/browser/part.test.ts b/src/vs/workbench/test/browser/part.test.ts index 38e6ef77cce..736ab2610f4 100644 --- a/src/vs/workbench/test/browser/part.test.ts +++ b/src/vs/workbench/test/browser/part.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Part } from 'vs/workbench/browser/part'; import { isEmptyObject } from 'vs/base/common/types'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; @@ -115,7 +115,7 @@ suite('Workbench parts', () => { }); teardown(() => { - mainWindow.document.body.removeChild(fixture); + fixture.remove(); disposables.clear(); }); diff --git a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts index 47eb7efceba..2b68bdea2dd 100644 --- a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { BreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; diff --git a/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts index be643126620..60d9d133981 100644 --- a/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/test/browser/parts/editor/editor.test.ts b/src/vs/workbench/test/browser/parts/editor/editor.test.ts index eaf064119d0..00add777446 100644 --- a/src/vs/workbench/test/browser/parts/editor/editor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editor.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EditorResourceAccessor, SideBySideEditor, EditorInputWithPreferredResource, EditorInputCapabilities, isEditorIdentifier, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, isResourceEditorInput, isUntitledResourceEditorInput, isResourceDiffEditorInput, isEditorInputWithOptionsAndGroup, EditorInputWithOptions, isEditorInputWithOptions, isEditorInput, EditorInputWithOptionsAndGroup, isResourceSideBySideEditorInput, IResourceSideBySideEditorInput, isTextEditorViewState, isResourceMergeEditorInput, IResourceMergeEditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts index b6ece8430e7..a2047327ff1 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts index 22a0fd80cf9..f87bac4abaa 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EditorGroupModel, IGroupEditorChangeEvent, IGroupEditorCloseEvent, IGroupEditorMoveEvent, IGroupEditorOpenEvent, ISerializedEditorGroupModel, isGroupEditorChangeEvent, isGroupEditorCloseEvent, isGroupEditorMoveEvent, isGroupEditorOpenEvent } from 'vs/workbench/common/editor/editorGroupModel'; import { EditorExtensions, IEditorFactoryRegistry, IFileEditorInput, IEditorSerializer, CloseDirection, EditorsOrder, IResourceDiffEditorInput, IResourceSideBySideEditorInput, SideBySideEditor, EditorCloseContext, GroupModelChangeKind } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts index 4c1b74c31e7..e66b98bd3ad 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts index f293125aad4..05cc0e521e6 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IModelService } from 'vs/editor/common/services/model'; diff --git a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts index 3d817b86fea..2aae30b5243 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EditorPane, EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; import { WorkspaceTrustRequiredPlaceholderEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; import { IEditorSerializer, IEditorFactoryRegistry, EditorExtensions, EditorInputCapabilities, IEditorDescriptor, IEditorPane } from 'vs/workbench/common/editor'; diff --git a/src/vs/workbench/test/browser/parts/editor/filteredEditorGroupModel.test.ts b/src/vs/workbench/test/browser/parts/editor/filteredEditorGroupModel.test.ts index 80765957797..53db7729f74 100644 --- a/src/vs/workbench/test/browser/parts/editor/filteredEditorGroupModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/filteredEditorGroupModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EditorGroupModel, ISerializedEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; import { EditorExtensions, IEditorFactoryRegistry, IFileEditorInput, IEditorSerializer, EditorsOrder, GroupModelChangeKind } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts index 2002d562f55..c3dd4fd67a8 100644 --- a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts index cbd6264885f..4be1b5c4fc4 100644 --- a/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts index 60ab56b329a..a13dc9884dc 100644 --- a/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { workbenchInstantiationService, TestServiceAccessor, registerTestFileEditor, createEditorPart, TestTextFileEditor } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts index 029ad438984..24d70f5a76e 100644 --- a/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel'; diff --git a/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts b/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts index 3f0f767be42..67a0a5140e1 100644 --- a/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts +++ b/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { StatusbarViewModel } from 'vs/workbench/browser/parts/statusbar/statusbarModel'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; diff --git a/src/vs/workbench/test/browser/quickAccess.test.ts b/src/vs/workbench/test/browser/quickAccess.test.ts index 9d71c84ccf3..49c78111274 100644 --- a/src/vs/workbench/test/browser/quickAccess.test.ts +++ b/src/vs/workbench/test/browser/quickAccess.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions, IQuickAccessProvider, QuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickPick, IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; diff --git a/src/vs/workbench/test/browser/viewlet.test.ts b/src/vs/workbench/test/browser/viewlet.test.ts index eefc87cb2d6..0c970c81ce7 100644 --- a/src/vs/workbench/test/browser/viewlet.test.ts +++ b/src/vs/workbench/test/browser/viewlet.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Registry } from 'vs/platform/registry/common/platform'; import { PaneCompositeDescriptor, Extensions, PaneCompositeRegistry, PaneComposite } from 'vs/workbench/browser/panecomposite'; import { isFunction } from 'vs/base/common/types'; diff --git a/src/vs/workbench/test/browser/webview.test.ts b/src/vs/workbench/test/browser/webview.test.ts index 3d0c3701f14..31d546e934f 100644 --- a/src/vs/workbench/test/browser/webview.test.ts +++ b/src/vs/workbench/test/browser/webview.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { parentOriginHash } from 'vs/base/browser/iframe'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/test/browser/window.test.ts b/src/vs/workbench/test/browser/window.test.ts index f65d50c3aea..fe3c2ff236a 100644 --- a/src/vs/workbench/test/browser/window.test.ts +++ b/src/vs/workbench/test/browser/window.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IRegisteredCodeWindow } from 'vs/base/browser/dom'; import { CodeWindow, mainWindow } from 'vs/base/browser/window'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index d6786841e1c..9560047ea60 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -400,7 +400,8 @@ export class TestServiceAccessor { @IInstantiationService public instantiationService: IInstantiationService, @IElevatedFileService public elevatedFileService: IElevatedFileService, @IWorkspaceTrustRequestService public workspaceTrustRequestService: TestWorkspaceTrustRequestService, - @IDecorationsService public decorationsService: IDecorationsService + @IDecorationsService public decorationsService: IDecorationsService, + @IProgressService public progressService: IProgressService, ) { } } @@ -1557,6 +1558,10 @@ export class TestHostService implements IHostService { readonly colorScheme = ColorScheme.DARK; onDidChangeColorScheme = Event.None; + + getPathForFile(file: File): string | undefined { + return undefined; + } } export class TestFilesConfigurationService extends FilesConfigurationService { @@ -2130,7 +2135,6 @@ export class TestRemoteExtensionsScannerService implements IRemoteExtensionsScan declare readonly _serviceBrand: undefined; async whenExtensionsReady(): Promise { } scanExtensions(): Promise { throw new Error('Method not implemented.'); } - scanSingleExtension(): Promise { throw new Error('Method not implemented.'); } } export class TestWorkbenchExtensionEnablementService implements IWorkbenchExtensionEnablementService { diff --git a/src/vs/workbench/test/common/memento.test.ts b/src/vs/workbench/test/common/memento.test.ts index 84502b1c321..7850379177f 100644 --- a/src/vs/workbench/test/common/memento.test.ts +++ b/src/vs/workbench/test/common/memento.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { StorageScope, IStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index de27e6b87cd..ac087cde929 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { NotificationsModel, NotificationViewItem, INotificationChangeEvent, NotificationChangeType, NotificationViewItemContentChangeKind, IStatusMessageChangeEvent, StatusMessageChangeType, INotificationsFilter } from 'vs/workbench/common/notifications'; import { Action } from 'vs/base/common/actions'; import { INotification, Severity, NotificationsFilter, NotificationPriority } from 'vs/platform/notification/common/notification'; diff --git a/src/vs/workbench/test/common/resources.test.ts b/src/vs/workbench/test/common/resources.test.ts index 6ce08bcf9ea..82cb89a37a3 100644 --- a/src/vs/workbench/test/common/resources.test.ts +++ b/src/vs/workbench/test/common/resources.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/test/common/utils.ts b/src/vs/workbench/test/common/utils.ts index b28eb1d1c8b..697812f153c 100644 --- a/src/vs/workbench/test/common/utils.ts +++ b/src/vs/workbench/test/common/utils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { LanguagesRegistry } from 'vs/editor/common/services/languagesRegistry'; /** diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index 1a938d7dbd6..2caf3646a28 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -30,6 +30,7 @@ import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IAutoSaveConfiguration, IAutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, IWorkspaceTrustUriInfo, WorkspaceTrustRequestOptions, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; import { IMarker, IMarkerData, IMarkerService, IResourceMarker, MarkerStatistics } from 'vs/platform/markers/common/markers'; +import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; export class TestLoggerService extends AbstractLoggerService { constructor(logsHome?: URI) { @@ -253,7 +254,7 @@ export class TestWorkingCopyFileService implements IWorkingCopyFileService { readonly hasSaveParticipants = false; addSaveParticipant(participant: IStoredFileWorkingCopySaveParticipant): IDisposable { return Disposable.None; } - async runSaveParticipants(workingCopy: IWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, token: CancellationToken): Promise { } + async runSaveParticipants(workingCopy: IWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { } async delete(operations: IDeleteOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise { } diff --git a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts index 14536883bea..ff49db5e3c7 100644 --- a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts +++ b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NativeWindow } from 'vs/workbench/electron-sandbox/window'; import { ITunnelService, RemoteTunnel } from 'vs/platform/tunnel/common/tunnel'; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 1c4ab3a446a..a9092bc788b 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -205,6 +205,9 @@ import 'vs/workbench/contrib/inlineChat/browser/inlineChat.contribution'; // Interactive import 'vs/workbench/contrib/interactive/browser/interactive.contribution'; +// repl +import 'vs/workbench/contrib/replNotebook/browser/repl.contribution'; + // Testing import 'vs/workbench/contrib/testing/browser/testing.contribution'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index ee470572dd0..a57c0618f7a 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -118,6 +118,9 @@ import 'vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution // Issues import 'vs/workbench/contrib/issue/electron-sandbox/issue.contribution'; +// Process +import 'vs/workbench/contrib/issue/electron-sandbox/process.contribution'; + // Remote import 'vs/workbench/contrib/remote/electron-sandbox/remote.contribution'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 3ea1c3f1fee..d018cf570fd 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -156,6 +156,7 @@ import 'vs/workbench/contrib/tags/browser/workspaceTagsService'; // Issues import 'vs/workbench/contrib/issue/browser/issue.contribution'; + // Splash import 'vs/workbench/contrib/splash/browser/splash.contribution'; diff --git a/src/vscode-dts/README.md b/src/vscode-dts/README.md index 9b3640d9208..7d8c057c480 100644 --- a/src/vscode-dts/README.md +++ b/src/vscode-dts/README.md @@ -14,7 +14,7 @@ This is the place for the stable API and for API proposals. ## Add a new proposal 1. create a _new_ file in this directory, its name must follow this pattern `vscode.proposed.[a-zA-Z]+.d.ts` -1. creating the proposal-file will automatically update `src/vs/workbench/services/extensions/common/extensionsApiProposals.ts` (make sure to run `yarn watch`) +1. creating the proposal-file will automatically update `src/vs/platform/extensions/common/extensionsApiProposals.ts` (make sure to run `yarn watch`) 1. declare and implement your proposal 1. make sure to use the `checkProposedApiEnabled` and/or `isProposedApiEnabled`-utils to enforce the API being proposed. Make sure to invoke them with your proposal's name which got generated into `extensionsApiProposals.ts` 1. Most likely will need to add your proposed api to vscode-api-tests as well diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 50e8060b8ee..8c22232e2b6 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -535,7 +535,7 @@ declare module 'vscode' { /** * Represents sources that can cause {@link window.onDidChangeTextEditorSelection selection change events}. - */ + */ export enum TextEditorSelectionChangeKind { /** * Selection changed due to typing in the editor. @@ -5186,7 +5186,7 @@ declare module 'vscode' { /** * Creates a new list of inline completion items. - */ + */ constructor(items: InlineCompletionItem[]); } @@ -6365,7 +6365,7 @@ declare module 'vscode' { export enum ConfigurationTarget { /** * Global configuration - */ + */ Global = 1, /** @@ -11116,8 +11116,8 @@ declare module 'vscode' { canSelectMany?: boolean; /** - * An optional interface to implement drag and drop in the tree view. - */ + * An optional interface to implement drag and drop in the tree view. + */ dragAndDropController?: TreeDragAndDropController; /** @@ -11378,8 +11378,8 @@ declare module 'vscode' { */ export interface TreeCheckboxChangeEvent { /** - * The items that were checked or unchecked. - */ + * The items that were checked or unchecked. + */ readonly items: ReadonlyArray<[T, TreeItemCheckboxState]>; } @@ -11419,8 +11419,8 @@ declare module 'vscode' { readonly onDidChangeVisibility: Event; /** - * An event to signal that an element or root has either been checked or unchecked. - */ + * An event to signal that an element or root has either been checked or unchecked. + */ readonly onDidChangeCheckboxState: Event>; /** @@ -11697,8 +11697,8 @@ declare module 'vscode' { } /** - * Checkbox state of the tree item - */ + * Checkbox state of the tree item + */ export enum TreeItemCheckboxState { /** * Determines an item is unchecked @@ -11787,8 +11787,8 @@ declare module 'vscode' { color?: ThemeColor; /** - * The {@link TerminalLocation} or {@link TerminalEditorLocationOptions} or {@link TerminalSplitLocationOptions} for the terminal. - */ + * The {@link TerminalLocation} or {@link TerminalEditorLocationOptions} or {@link TerminalSplitLocationOptions} for the terminal. + */ location?: TerminalLocation | TerminalEditorLocationOptions | TerminalSplitLocationOptions; /** @@ -12668,7 +12668,7 @@ declare module 'vscode' { /** * The reason why the document was changed. * Is `undefined` if the reason is not known. - */ + */ readonly reason: TextDocumentChangeReason | undefined; } @@ -15374,7 +15374,7 @@ declare module 'vscode' { * * @param rendererId The renderer ID to communicate with * @returns A new notebook renderer messaging object. - */ + */ export function createRendererMessaging(rendererId: string): NotebookRendererMessaging; } @@ -16183,6 +16183,13 @@ declare module 'vscode' { * When true, the debug viewlet will not be automatically revealed for this session. */ suppressDebugView?: boolean; + + /** + * Signals to the editor that the debug session was started from a test run + * request. This is used to link the lifecycle of the debug session and + * test run in UI actions. + */ + testRun?: TestRun; } /** @@ -16369,7 +16376,7 @@ declare module 'vscode' { /** * Add breakpoints. * @param breakpoints The breakpoints to add. - */ + */ export function addBreakpoints(breakpoints: readonly Breakpoint[]): void; /** @@ -16907,17 +16914,17 @@ declare module 'vscode' { /** * Whether it is possible to be signed into multiple accounts at once with this provider. * If not specified, will default to false. - */ + */ readonly supportsMultipleAccounts?: boolean; } /** - * An {@link Event} which fires when an {@link AuthenticationSession} is added, removed, or changed. - */ + * An {@link Event} which fires when an {@link AuthenticationSession} is added, removed, or changed. + */ export interface AuthenticationProviderAuthenticationSessionsChangeEvent { /** * The {@link AuthenticationSession AuthenticationSessions} of the {@link AuthenticationProvider} that have been added. - */ + */ readonly added: readonly AuthenticationSession[] | undefined; /** @@ -17144,7 +17151,7 @@ declare module 'vscode' { * @param id Identifier for the controller, must be globally unique. * @param label A human-readable label for the controller. * @returns An instance of the {@link TestController}. - */ + */ export function createTestController(id: string, label: string): TestController; } @@ -18995,7 +19002,7 @@ declare module 'vscode' { * Represents a language model response. * * @see {@link LanguageModelAccess.chatRequest} - */ + */ export interface LanguageModelChatResponse { /** diff --git a/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts b/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts index 93f9761211b..a75d7eb6f7e 100644 --- a/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts @@ -5,99 +5,6 @@ declare module 'vscode' { - // https://github.com/microsoft/vscode/issues/205317 - - /** - * The parameters of a query for text search. - */ - export interface TextSearchQuery { - /** - * The text pattern to search for. - */ - pattern: string; - - /** - * Whether or not `pattern` should match multiple lines of text. - */ - isMultiline?: boolean; - - /** - * Whether or not `pattern` should be interpreted as a regular expression. - */ - isRegExp?: boolean; - - /** - * Whether or not the search should be case-sensitive. - */ - isCaseSensitive?: boolean; - - /** - * Whether or not to search for whole word matches only. - */ - isWordMatch?: boolean; - } - - /** - * Options common to file and text search - */ - export interface SearchOptions { - /** - * The root folder to search within. - */ - folder: Uri; - - /** - * Files that match an `includes` glob pattern should be included in the search. - */ - includes: GlobString[]; - - /** - * Files that match an `excludes` glob pattern should be excluded from the search. - */ - excludes: GlobString[]; - - /** - * Whether external files that exclude files, like .gitignore, should be respected. - * See the vscode setting `"search.useIgnoreFiles"`. - */ - useIgnoreFiles: boolean; - - /** - * Whether symlinks should be followed while searching. - * See the vscode setting `"search.followSymlinks"`. - */ - followSymlinks: boolean; - - /** - * Whether global files that exclude files, like .gitignore, should be respected. - * See the vscode setting `"search.useGlobalIgnoreFiles"`. - */ - useGlobalIgnoreFiles: boolean; - - /** - * Whether files in parent directories that exclude files, like .gitignore, should be respected. - * See the vscode setting `"search.useParentIgnoreFiles"`. - */ - useParentIgnoreFiles: boolean; - } - - /** - * Options to specify the size of the result text preview. - * These options don't affect the size of the match itself, just the amount of preview text. - */ - export interface TextSearchPreviewOptions { - /** - * The maximum number of lines in the preview. - * Only search providers that support multiline search will ever return more than one line in the match. - */ - matchLines: number; - - /** - * The maximum number of characters included per line. - */ - charsPerLine: number; - } - /** * Options that apply to AI text search. */ @@ -129,109 +36,6 @@ declare module 'vscode' { } - - /** - * A message regarding a completed search. - */ - export interface TextSearchCompleteMessage { - /** - * Markdown text of the message. - */ - text: string; - /** - * Whether the source of the message is trusted, command links are disabled for untrusted message sources. - * Messaged are untrusted by default. - */ - trusted?: boolean; - /** - * The message type, this affects how the message will be rendered. - */ - type: TextSearchCompleteMessageType; - } - - /** - * Information collected when text search is complete. - */ - export interface TextSearchComplete { - /** - * Whether the search hit the limit on the maximum number of search results. - * `maxResults` on {@linkcode AITextSearchOptions} specifies the max number of results. - * - If exactly that number of matches exist, this should be false. - * - If `maxResults` matches are returned and more exist, this should be true. - * - If search hits an internal limit which is less than `maxResults`, this should be true. - */ - limitHit?: boolean; - - /** - * Additional information regarding the state of the completed search. - * - * Messages with "Information" style support links in markdown syntax: - * - Click to [run a command](command:workbench.action.OpenQuickPick) - * - Click to [open a website](https://aka.ms) - * - * Commands may optionally return { triggerSearch: true } to signal to the editor that the original search should run be again. - */ - message?: TextSearchCompleteMessage | TextSearchCompleteMessage[]; - } - - /** - * A preview of the text result. - */ - export interface TextSearchMatchPreview { - /** - * The matching lines of text, or a portion of the matching line that contains the match. - */ - text: string; - - /** - * The Range within `text` corresponding to the text of the match. - * The number of matches must match the TextSearchMatch's range property. - */ - matches: Range | Range[]; - } - - /** - * A match from a text search - */ - export interface TextSearchMatch { - /** - * The uri for the matching document. - */ - uri: Uri; - - /** - * The range of the match within the document, or multiple ranges for multiple matches. - */ - ranges: Range | Range[]; - - /** - * A preview of the text match. - */ - preview: TextSearchMatchPreview; - } - - /** - * A line of context surrounding a TextSearchMatch. - */ - export interface TextSearchContext { - /** - * The uri for the matching document. - */ - uri: Uri; - - /** - * One line of text. - * previewOptions.charsPerLine applies to this - */ - text: string; - - /** - * The line number of this line of context. - */ - lineNumber: number; - } - - /** * An AITextSearchProvider provides additional AI text search results in the workspace. */ diff --git a/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts b/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts index 63000738c0f..b717159885b 100644 --- a/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts +++ b/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts @@ -6,23 +6,41 @@ declare module 'vscode' { export class FileCoverage2 extends FileCoverage { /** - * Test {@link TestItem} this file coverage is generated from. If undefined, - * the editor will assume the coverage is the overall summary coverage for - * the entire file. - * - * If per-test coverage is available, an extension should append multiple - * `FileCoverage` instances with this property set for each test item. It - * must also append a `FileCoverage` instance without this property set to - * represent the overall coverage of the file. + * A list of {@link TestItem test cases} that generated coverage in this + * file. If set, then {@link TestRunProfile.loadDetailedCoverageForTest} + * should also be defined in order to retrieve detailed coverage information. */ - testItem?: TestItem; + fromTests: TestItem[]; constructor( uri: Uri, statementCoverage: TestCoverageCount, branchCoverage?: TestCoverageCount, declarationCoverage?: TestCoverageCount, - testItem?: TestItem, + fromTests?: TestItem[], ); } + + export interface TestRunProfile { + /** + * An extension-provided function that provides detailed statement and + * function-level coverage for a single test in a file. This is the per-test + * sibling of {@link TestRunProfile.loadDetailedCoverage}, called only if + * a test item is provided in {@link FileCoverage.fromTests} and only for + * files where such data is reported. + * + * The editor will call this when user asks to view coverage for a test in + * a file, and the returned coverage information is used to display exactly + * what code was run by that test. + * + * The {@link FileCoverage} object passed to this function is the same + * instance emitted on {@link TestRun.addCoverage} calls associated with this profile. + * + * @param testRun The test run that generated the coverage data. + * @param fileCoverage The file coverage object to load detailed coverage for. + * @param fromTestItem The test item to request coverage information for. + * @param token A cancellation token that indicates the operation should be cancelled. + */ + loadDetailedCoverageForTest?: (testRun: TestRun, fileCoverage: FileCoverage, fromTestItem: TestItem, token: CancellationToken) => Thenable; + } } diff --git a/src/vscode-dts/vscode.proposed.authGetSessions.d.ts b/src/vscode-dts/vscode.proposed.authGetSessions.d.ts index 20a692c0180..6d425ed3232 100644 --- a/src/vscode-dts/vscode.proposed.authGetSessions.d.ts +++ b/src/vscode-dts/vscode.proposed.authGetSessions.d.ts @@ -7,42 +7,50 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/152399 - export interface AuthenticationForceNewSessionOptions { - /** - * The session that you are asking to be recreated. The Auth Provider can use this to - * help guide the user to log in to the correct account. - */ - sessionToRecreate?: AuthenticationSession; - } + // FOR THE CONSUMER export namespace authentication { /** - * Get all authentication sessions matching the desired scopes that this extension has access to. In order to request access, - * use {@link getSession}. To request an additional account, specify {@link AuthenticationGetSessionOptions.clearSessionPreference} - * and {@link AuthenticationGetSessionOptions.createIfNone} together. + * Get all accounts that the user is logged in to for the specified provider. + * Use this paired with {@link getSession} in order to get an authentication session for a specific account. * * Currently, there are only two authentication providers that are contributed from built in extensions * to the editor that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. * - * @param providerId The id of the provider to use - * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider - * @returns A thenable that resolves to a readonly array of authentication sessions. + * Note: Getting accounts does not imply that your extension has access to that account or its authentication sessions. You can verify access to the account by calling {@link getSession}. + * + * @param providerId The id of the provider to use + * @returns A thenable that resolves to a readonly array of authentication accounts. + */ + export function getAccounts(providerId: string): Thenable; + } + + export interface AuthenticationGetSessionOptions { + /** + * The account that you would like to get a session for. This is passed down to the Authentication Provider to be used for creating the correct session. */ - export function getSessions(providerId: string, scopes: readonly string[]): Thenable; + account?: AuthenticationSessionAccountInformation; } - /** - * The options passed in to the provider when creating a session. - */ - export interface AuthenticationProviderCreateSessionOptions { + // FOR THE AUTH PROVIDER + + export interface AuthenticationProviderSessionOptions { /** - * The session that is being asked to be recreated. If this is passed in, the provider should - * attempt to recreate the session based on the information in this session. + * The account that is being asked about. If this is passed in, the provider should + * attempt to return the sessions that are only related to this account. */ - sessionToRecreate?: AuthenticationSession; + account?: AuthenticationSessionAccountInformation; } export interface AuthenticationProvider { + /** + * Get a list of sessions. + * @param scopes An optional list of scopes. If provided, the sessions returned should match + * these permissions, otherwise all sessions should be returned. + * @param options Additional options for getting sessions. + * @returns A promise that resolves to an array of authentication sessions. + */ + getSessions(scopes: readonly string[] | undefined, options: AuthenticationProviderSessionOptions): Thenable; /** * Prompts a user to login. * @@ -57,6 +65,6 @@ declare module 'vscode' { * @param options Additional options for creating a session. * @returns A promise that resolves to an authentication session. */ - createSession(scopes: readonly string[], options: AuthenticationProviderCreateSessionOptions): Thenable; + createSession(scopes: readonly string[], options: AuthenticationProviderSessionOptions): Thenable; } } diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 7fc7c3e3b97..9abbdc5d1aa 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -10,20 +10,29 @@ declare module 'vscode' { part: string; } + export interface ChatResponseFragment2 { + index: number; + part: LanguageModelChatResponseTextPart | LanguageModelChatResponseFunctionUsePart; + } + // @API extension ship a d.ts files for their options /** * Represents a large language model that accepts ChatML messages and produces a streaming response */ - export interface ChatResponseProvider { + export interface LanguageModelChatProvider { onDidReceiveLanguageModelResponse2?: Event<{ readonly extensionId: string; readonly participant?: string; readonly tokenCount?: number }>; provideLanguageModelResponse(messages: LanguageModelChatMessage[], options: { [name: string]: any }, extensionId: string, progress: Progress, token: CancellationToken): Thenable; + provideLanguageModelResponse2?(messages: LanguageModelChatMessage[], options: LanguageModelChatRequestOptions, extensionId: string, progress: Progress, token: CancellationToken): Thenable; + provideTokenCount(text: string | LanguageModelChatMessage, token: CancellationToken): Thenable; } + export type ChatResponseProvider = LanguageModelChatProvider; + export interface ChatResponseProviderMetadata { readonly vendor: string; @@ -64,14 +73,14 @@ declare module 'vscode' { export namespace chat { /** - * Register a LLM as chat response provider to the editor. - * - * - * @param id - * @param provider - * @param metadata - */ + * @deprecated use `lm.registerChatResponseProvider` instead + */ export function registerChatResponseProvider(id: string, provider: ChatResponseProvider, metadata: ChatResponseProviderMetadata): Disposable; } + export namespace lm { + + export function registerChatModelProvider(id: string, provider: LanguageModelChatProvider, metadata: ChatResponseProviderMetadata): Disposable; + } + } diff --git a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts index 1b404980e2c..62f07234bb1 100644 --- a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts +++ b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts @@ -19,15 +19,6 @@ declare module 'vscode' { * @param icon An icon to display when selecting context in the picker UI. */ export function registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: ChatVariableResolver, fullName?: string, icon?: ThemeIcon): Disposable; - - /** - * Attaches a chat context with the specified name, value, and location. - * - * @param name - The name of the chat context. - * @param value - The value of the chat context. - * @param location - The location of the chat context. - */ - export function attachContext(name: string, value: string | Uri | Location | unknown, location: ChatLocation.Panel): void; } export interface ChatVariableValue { diff --git a/src/vscode-dts/vscode.proposed.commentReveal.d.ts b/src/vscode-dts/vscode.proposed.commentReveal.d.ts new file mode 100644 index 00000000000..168c4691de5 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.commentReveal.d.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // @alexr00 https://github.com/microsoft/vscode/issues/167253 + + /** + * Options to reveal a comment thread in an editor. + */ + export interface CommentThreadRevealOptions { + /** + * By default, the comment thread will be focused. Set `preserveFocus` to `true` to maintain the original focus. + */ + preserveFocus?: boolean; + + /** + * Focus the comment thread reply editor, if the thread supports replying. + */ + focusReply?: boolean; + } + + export interface CommentThread { + /** + * Reveal the comment thread in an editor. + */ + reveal(options?: CommentThreadRevealOptions): Thenable; + } + +} diff --git a/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts b/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts index e09f5a34d6b..547ee182227 100644 --- a/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts +++ b/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts @@ -28,5 +28,15 @@ declare module 'vscode' { * Worth noting that we already have this problem for the `comments` property. */ state?: CommentThreadState | { resolved?: CommentThreadState; applicability?: CommentThreadApplicability }; + readonly uri: Uri; + range: Range | undefined; + comments: readonly Comment[]; + collapsibleState: CommentThreadCollapsibleState; + canReply: boolean; + contextValue?: string; + label?: string; + dispose(): void; + // Part of the comment reveal proposal + reveal(options?: CommentThreadRevealOptions): Thenable; } } diff --git a/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts b/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts index 8ef9b90a8ea..d138471f2af 100644 --- a/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts +++ b/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts @@ -28,7 +28,7 @@ declare module 'vscode' { * An optional set of glob patterns to exclude from watching. * Glob patterns are always matched relative to the watched folder. */ - readonly excludes?: string[]; + readonly excludes: string[]; } export namespace workspace { diff --git a/src/vscode-dts/vscode.proposed.fileComments.d.ts b/src/vscode-dts/vscode.proposed.fileComments.d.ts index 7370f22f762..8f8c7d2d943 100644 --- a/src/vscode-dts/vscode.proposed.fileComments.d.ts +++ b/src/vscode-dts/vscode.proposed.fileComments.d.ts @@ -6,68 +6,27 @@ declare module 'vscode' { export interface CommentThread2 { - /** - * The uri of the document the thread has been created on. - */ - readonly uri: Uri; - /** * The range the comment thread is located within the document. The thread icon will be shown - * at the last line of the range. + * at the last line of the range. When set to undefined, the comment will be associated with the + * file, and not a specific range. */ range: Range | undefined; + } + /** + * The ranges a CommentingRangeProvider enables commenting on. + */ + export interface CommentingRanges { /** - * The ordered comments of the thread. - */ - comments: readonly Comment[]; - - /** - * Whether the thread should be collapsed or expanded when opening the document. - * Defaults to Collapsed. - */ - collapsibleState: CommentThreadCollapsibleState; - - /** - * Whether the thread supports reply. - * Defaults to true. - */ - canReply: boolean; - - /** - * Context value of the comment thread. This can be used to contribute thread specific actions. - * For example, a comment thread is given a context value as `editable`. When contributing actions to `comments/commentThread/title` - * using `menus` extension point, you can specify context value for key `commentThread` in `when` expression like `commentThread == editable`. - * ```json - * "contributes": { - * "menus": { - * "comments/commentThread/title": [ - * { - * "command": "extension.deleteCommentThread", - * "when": "commentThread == editable" - * } - * ] - * } - * } - * ``` - * This will show action `extension.deleteCommentThread` only for comment threads with `contextValue` is `editable`. - */ - contextValue?: string; - - /** - * The optional human-readable label describing the {@link CommentThread Comment Thread} + * Enables comments to be added to a file without a specific range. */ - label?: string; - - // from the commentThreadRelevance proposal - state?: CommentThreadState | { resolved?: CommentThreadState; applicability?: CommentThreadApplicability }; + enableFileComments: boolean; /** - * Dispose this comment thread. - * - * Once disposed, this comment thread will be removed from visible editors and Comment Panel when appropriate. + * The ranges which allow new comment threads creation. */ - dispose(): void; + ranges?: Range[] } export interface CommentController { @@ -78,6 +37,6 @@ declare module 'vscode' { /** * Provide a list of ranges which allow new comment threads creation or null for a given document */ - provideCommentingRanges(document: TextDocument, token: CancellationToken): ProviderResult; + provideCommentingRanges(document: TextDocument, token: CancellationToken): ProviderResult; } } diff --git a/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts b/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts index bf7bc5ecba3..8dcfd99852b 100644 --- a/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts @@ -13,6 +13,12 @@ declare module 'vscode' { export interface FileSearchQuery { /** * The search pattern to match against file paths. + * To be correctly interpreted by Quick Open, this is interpreted in a relaxed way. The picker will apply its own highlighting and scoring on the results. + * + * Tips for matching in Quick Open: + * With the pattern, the picker will use the file name and file paths to score each entry. The score will determine the ordering and filtering. + * The scoring prioritizes prefix and substring matching. Then, it checks and it checks whether the pattern's letters appear in the same order as in the target (file name and path). + * If a file does not match at all using our criteria, it will be omitted from Quick Open. */ pattern: string; } diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts index 2715014a0a8..eccc51b5380 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts @@ -60,6 +60,12 @@ declare module 'vscode' { */ // eslint-disable-next-line local/vscode-dts-provider-naming handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, info: PartialAcceptInfo): void; + + provideInlineEdits?(document: TextDocument, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + } + + export interface InlineCompletionContext { + readonly userPrompt?: string; } export interface PartialAcceptInfo { diff --git a/src/vscode-dts/vscode.proposed.lmTools.d.ts b/src/vscode-dts/vscode.proposed.lmTools.d.ts new file mode 100644 index 00000000000..facbeb20c4d --- /dev/null +++ b/src/vscode-dts/vscode.proposed.lmTools.d.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// version: 2 +// https://github.com/microsoft/vscode/issues/213274 + +declare module 'vscode' { + + // TODO@API capabilities + + export type JSONSchema = object; + + // API -> LM: an tool/function that is available to the language model + export interface LanguageModelChatFunction { + name: string; + description: string; + parametersSchema?: JSONSchema; + } + + // API -> LM: add tools as request option + export interface LanguageModelChatRequestOptions { + // TODO@API this will a heterogeneous array of different types of tools + tools?: LanguageModelChatFunction[]; + } + + // LM -> USER: function that should be used + export class LanguageModelChatResponseFunctionUsePart { + name: string; + parameters: any; + + constructor(name: string, parameters: any); + } + + // LM -> USER: text chunk + export class LanguageModelChatResponseTextPart { + value: string; + + constructor(value: string); + } + + export interface LanguageModelChatResponse { + + stream: AsyncIterable; + } + + + // USER -> LM: the result of a function call + export class LanguageModelChatMessageFunctionResultPart { + name: string; + content: string; + isError: boolean; + + constructor(name: string, content: string, isError?: boolean); + } + + export interface LanguageModelChatMessage { + content2: string | LanguageModelChatMessageFunctionResultPart; + } + + // Tool registration/invoking between extensions + + export namespace lm { + /** + * Register a LanguageModelTool. The tool must also be registered in the package.json `languageModelTools` contribution point. + */ + export function registerTool(name: string, tool: LanguageModelTool): Disposable; + + /** + * A list of all available tools. + */ + export const tools: ReadonlyArray; + + /** + * Invoke a tool with the given parameters. + */ + export function invokeTool(name: string, parameters: Object, token: CancellationToken): Thenable; + } + + // Is the same as LanguageModelChatFunction now, but could have more details in the future + export interface LanguageModelToolDescription { + name: string; + description: string; + parametersSchema?: JSONSchema; + } + + export interface LanguageModelTool { + invoke(parameters: any, token: CancellationToken): Thenable; + } +} diff --git a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index 653f6a2d50d..6ed185785d8 100644 --- a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -23,7 +23,7 @@ declare module 'vscode' { * Provide mapped edits for a given document. * @param document The document to provide mapped edits for. * @param codeBlocks Code blocks that come from an LLM's reply. - * "Insert at cursor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. + * "Apply in Editor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. * @param context The context for providing mapped edits. * @param token A cancellation token. * @returns A provider result of text edits. diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index e7e7ca1c85b..9d3151733aa 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -25,6 +25,7 @@ declare module 'vscode' { // onDidChangeHistoryItemGroups: Event; provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; + provideHistoryItems2(options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; provideHistoryItemSummary?(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; @@ -34,17 +35,14 @@ declare module 'vscode' { export interface SourceControlHistoryOptions { readonly cursor?: string; readonly limit?: number | { id?: string }; + readonly historyItemGroupIds?: readonly string[]; } export interface SourceControlHistoryItemGroup { readonly id: string; readonly name: string; - readonly base?: Omit; - } - - export interface SourceControlRemoteHistoryItemGroup { - readonly id: string; - readonly name: string; + readonly base?: Omit, 'remote'>; + readonly remote?: Omit, 'remote'>; } export interface SourceControlHistoryItemStatistics { @@ -53,6 +51,11 @@ declare module 'vscode' { readonly deletions: number; } + export interface SourceControlHistoryItemLabel { + readonly title: string; + readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon; + } + export interface SourceControlHistoryItem { readonly id: string; readonly parentIds: string[]; @@ -61,6 +64,7 @@ declare module 'vscode' { readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon; readonly timestamp?: number; readonly statistics?: SourceControlHistoryItemStatistics; + readonly labels?: SourceControlHistoryItemLabel[]; } export interface SourceControlHistoryItemChange { diff --git a/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts b/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts index 9ea09344b8a..8e44e8e48d6 100644 --- a/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts @@ -29,9 +29,9 @@ declare module 'vscode' { * }); * function summarizeCommandLine(commandLine: TerminalShellExecutionCommandLine) { * return [ - * ` Command line: ${command.ommandLine.value}`, - * ` Confidence: ${command.ommandLine.confidence}`, - * ` Trusted: ${command.ommandLine.isTrusted} + * ` Command line: ${command.commandLine.value}`, + * ` Confidence: ${command.commandLine.confidence}`, + * ` Trusted: ${command.commandLine.isTrusted} * ].join('\n'); * } */ @@ -125,7 +125,7 @@ declare module 'vscode' { /** * An object that contains [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered * features for the terminal. This will always be `undefined` immediately after the terminal - * is created. Listen to {@link window.onDidActivateTerminalShellIntegration} to be notified + * is created. Listen to {@link window.onDidChangeTerminalShellIntegration} to be notified * when shell integration is activated for a terminal. * * Note that this object may remain undefined if shell integation never activates. For @@ -155,11 +155,13 @@ declare module 'vscode' { * @example * // Execute a command in a terminal immediately after being created * const myTerm = window.createTerminal(); - * window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => { + * window.onDidChangeTerminalShellIntegration(async ({ terminal, shellIntegration }) => { * if (terminal === myTerm) { - * const command = shellIntegration.executeCommand('echo "Hello world"'); - * const code = await command.exitCode; - * console.log(`Command exited with code ${code}`); + * const execution = shellIntegration.executeCommand('echo "Hello world"'); + * window.onDidEndTerminalShellExecution(event => { + * if (event.execution === execution) { + * console.log(`Command exited with code ${event.exitCode}`); + * } * } * })); * // Fallback to sendText if there is no shell integration within 3 seconds of launching @@ -175,9 +177,11 @@ declare module 'vscode' { * // Send command to terminal that has been alive for a while * const commandLine = 'echo "Hello world"'; * if (term.shellIntegration) { - * const command = term.shellIntegration.executeCommand({ commandLine }); - * const code = await command.exitCode; - * console.log(`Command exited with code ${code}`); + * const execution = shellIntegration.executeCommand({ commandLine }); + * window.onDidEndTerminalShellExecution(event => { + * if (event.execution === execution) { + * console.log(`Command exited with code ${event.exitCode}`); + * } * } else { * term.sendText(commandLine); * // Without shell integration, we can't know when the command has finished or what the @@ -284,8 +288,29 @@ declare module 'vscode' { readonly execution: TerminalShellExecution; /** - * The exit code reported by the shell. `undefined` means the shell did not report an exit - * code or the shell reported a command started before the command finished. + * The exit code reported by the shell. + * + * Note that `undefined` means the shell either did not report an exit code (ie. the shell + * integration script is misbehaving) or the shell reported a command started before the command + * finished (eg. a sub-shell was opened). Generally this should not happen, depending on the use + * case, it may be best to treat this as a failure. + * + * @example + * const execution = shellIntegration.executeCommand({ + * command: 'echo', + * args: ['Hello world'] + * }); + * window.onDidEndTerminalShellExecution(event => { + * if (event.execution === execution) { + * if (event.exitCode === undefined) { + * console.log('Command finished but exit code is unknown'); + * } else if (event.exitCode === 0) { + * console.log('Command succeeded'); + * } else { + * console.log('Command failed'); + * } + * } + * }); */ readonly exitCode: number | undefined; } diff --git a/test/automation/src/search.ts b/test/automation/src/search.ts index 567ec20fa91..5cf6018b72a 100644 --- a/test/automation/src/search.ts +++ b/test/automation/src/search.ts @@ -61,6 +61,21 @@ export class Search extends Viewlet { await this.submitSearch(); } + async hasActivityBarMoved() { + await this.code.waitForElement('.activitybar'); + + const elementBoundingBox = await this.code.driver.getElementXY('.activitybar'); + return elementBoundingBox !== null && elementBoundingBox.x === 48 && elementBoundingBox.y === 375; + } + + async waitForPageUp(): Promise { + await this.code.dispatchKeybinding('PageUp'); + } + + async waitForPageDown(): Promise { + await this.code.dispatchKeybinding('PageDown'); + } + async submitSearch(): Promise { await this.waitForInputFocus(INPUT); diff --git a/test/automation/src/terminal.ts b/test/automation/src/terminal.ts index 59c5d368167..0389a330544 100644 --- a/test/automation/src/terminal.ts +++ b/test/automation/src/terminal.ts @@ -119,6 +119,7 @@ export class Terminal { // Reset await this.code.dispatchKeybinding('Backspace'); } + await this.code.wait(100); await this.code.dispatchKeybinding(altKey ? 'Alt+Enter' : 'enter'); await this.quickinput.waitForQuickInputClosed(); if (commandId === TerminalCommandIdWithValue.NewWithProfile) { diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 3af2d83bcca..990b7cd19fd 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -105,6 +105,10 @@ async function runTestsInBrowser(browserType: BrowserType, endpoint: url.UrlWith console.error(`Error saving web client logs (${error})`); } + if (args.debug) { + return; + } + try { await browser.close(); } catch (error) { diff --git a/test/smoke/src/areas/search/search.test.ts b/test/smoke/src/areas/search/search.test.ts index 92c75f14404..78f79b61838 100644 --- a/test/smoke/src/areas/search/search.test.ts +++ b/test/smoke/src/areas/search/search.test.ts @@ -19,6 +19,17 @@ export function setup(logger: Logger) { retry(async () => cp.execSync('git reset --hard HEAD --quiet', { cwd: app.workspacePathOrFolder }), 0, 5); }); + it('verifies the sidebar moves to the right', async function () { + const app = this.app as Application; + await app.workbench.search.openSearchViewlet(); + + await app.code.dispatchKeybinding('PageUp'); + await app.workbench.search.hasActivityBarMoved(); + + await app.code.dispatchKeybinding('PageUp'); + await app.workbench.search.hasActivityBarMoved(); + }); + it('searches for body & checks for correct result number', async function () { const app = this.app as Application; await app.workbench.search.openSearchViewlet(); diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index 50c00e18826..a20f05256bc 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -206,6 +206,10 @@ async function loadTests(opts) { 'throw ListenerLeakError' ]); + const _allowedSuitesWithOutput = new Set([ + 'InteractiveChatController' + ]); + let _testsWithUnexpectedOutput = false; // --- Start Positron --- @@ -214,7 +218,9 @@ async function loadTests(opts) { for (const consoleFn of [console.log, console.error, console.info, console.warn, console.trace, console.debug]) { console[consoleFn.name] = function (msg) { - if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTest.title)) { + if (!currentTest) { + consoleFn.apply(console, arguments); + } else if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTest.title) && !_allowedSuitesWithOutput.has(currentTest.parent?.title)) { _testsWithUnexpectedOutput = true; // --- Start Positron --- _unexpectedOut = msg; diff --git a/test/unit/node/index.js b/test/unit/node/index.js index 14ceed7177c..e876bd1e007 100644 --- a/test/unit/node/index.js +++ b/test/unit/node/index.js @@ -228,7 +228,7 @@ function main() { // set up last test Mocha.suite('Loader', function () { test('should not explode while loading', function () { - assert.ok(!didErr, 'should not explode while loading'); + assert.ok(!didErr, `should not explode while loading: ${didErr}`); }); }); } diff --git a/yarn.lock b/yarn.lock index 09a62e7f990..46d9bdc50f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1152,13 +1152,6 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/graceful-fs@4.1.2": - version "4.1.2" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.2.tgz#fbc9575dbcc6d1d91dd768d30c5fc0c19f6c50bd" - integrity sha512-epDhsJAVxJsWfeqpzEDFhLnhHMbHie/VMFY+2Hvt5p7FemeW5ELM+6gcVYL/ZsUwdu3zrWpDE3VUTddXW+EMYg== - dependencies: - "@types/node" "*" - "@types/gulp-svgmin@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@types/gulp-svgmin/-/gulp-svgmin-1.2.1.tgz#e18f344ea09560554652406b37e1dc3253a6bda2" @@ -1632,10 +1625,10 @@ bindings "^1.5.0" node-addon-api "^6.0.0" -"@vscode/proxy-agent@^0.19.0": - version "0.19.1" - resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.19.1.tgz#d9640d85df1c48885580b68bb4b2b54e17f5332c" - integrity sha512-cs1VOx6d5n69HhgzK0cWeyfudJt+9LdJi/vtgRRxxwisWKg4h83B3+EUJ4udF5SEkJgMBp3oU0jheZVt43ImnQ== +"@vscode/proxy-agent@^0.21.0": + version "0.21.0" + resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.21.0.tgz#93c818b863ad20b42679032ecc1e3ecdc6306f12" + integrity sha512-9YcpBq+ZhMr3EQY/5ScyHc9kIIU/AcYOQn3DXq0N9tl81ViVsUvii3Fh+FAtD0YQ/qWtDfGxt8VCWZtuyh2D0g== dependencies: "@tootallnate/once" "^3.0.0" agent-base "^7.0.1" @@ -2037,40 +2030,47 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== -"@xterm/addon-image@0.9.0-beta.17": - version "0.9.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.17.tgz#343d0665a6060d4f893b4f2d32de6ccbbd00bb63" - integrity sha512-g0r2hpBcLABY5as4llsMP36RHtkWooEn7tf+7U0/hTndJoCAvs4uGDqZNQigFgeAM3lJ4PnRYh4lfnEh9bGt8A== - -"@xterm/addon-search@0.16.0-beta.17": - version "0.16.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.17.tgz#7cb01c7f498405909d37040884ee22d1889a36d2" - integrity sha512-wBfxmWOeqG6HHHE5mVamDJ75zBdHC35ERNy5/aTpQsQsyxrnV0Ks76c8ZVTaTu9wyBCAyx7UmZT42Ot80khY/g== - -"@xterm/addon-serialize@0.14.0-beta.17": - version "0.14.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.17.tgz#1cb8e35c0d118060a807adb340624fa7f80dd9c5" - integrity sha512-/c3W39kdRgGGYDoYjXb5HrUC421qwPn6NryAT4WJuJWnyMtFbe2DPwKsTfHuCBPiPyovS3a9j950Md3O3YXDZA== - -"@xterm/addon-unicode11@0.9.0-beta.17": - version "0.9.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.17.tgz#b5558148029a796c6a6d78e2a8b7255f92a51530" - integrity sha512-z7v8uojFVrO1aLSWtnz5MzSrfWRT8phde7kh9ufqHLBv7YYtMHxlPVjSuW8PZ2h4eY1LOZf6icUAzrmyJmJ7Kg== - -"@xterm/addon-webgl@0.19.0-beta.17": - version "0.19.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.17.tgz#68ad9e68dd1cf581b391971de33f5c04966b0d8e" - integrity sha512-X8ObRgoZl7UZTgdndM+mpSO3hLzAhWKoXXrGvUQg/7XabRKAPrQ2XvdyZm04nYwibE6Tpit2h5kkxjlVqupIig== - -"@xterm/headless@5.6.0-beta.17": - version "5.6.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.6.0-beta.17.tgz#bff1d67c9c061c57adff22571e733d54e3aba2b7" - integrity sha512-ehS7y/XRqX1ppx4RPiYc0vu0SdIQ91aA4lSN/2XNOf3IGdP0A38Q7a0T6mzqxRGZKiiyA0kTR1szr78wnY+wmA== - -"@xterm/xterm@5.6.0-beta.17": - version "5.6.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.17.tgz#67ce2e2ff45bd6cc9f26d455d5522c6c4a122ed9" - integrity sha512-+wAv8PhaGQSN9yXWIa8EFtT33pbrA4lZakMB1P05fr+DQ7zoH66QOAUoDY95uOf/4+S6Ihz8wzP2+FH8zETQEA== +"@xterm/addon-clipboard@0.2.0-beta.4": + version "0.2.0-beta.4" + resolved "https://registry.yarnpkg.com/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.4.tgz#9911baaebfbc07a698ae62366a596bfdeac8fa7e" + integrity sha512-p2KGTFUDK4YFthCgfsv2wT66JDTZPcIuoWeDT+TmSFbS1smDPTMCyM/rDDkGY+duHRcQsIMVzGC+2NRb/exX6A== + dependencies: + js-base64 "^3.7.5" + +"@xterm/addon-image@0.9.0-beta.21": + version "0.9.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.21.tgz#64fe50ee623f3e518574e1cbbe649cc0c0d60265" + integrity sha512-kTArrrS7K5+WYTTO8Ktt1aYxKTO4/jUm3KmyvPVjf9iw7OhLtG9mU+X9dXo56DTAqmbIUfJgY3OQbWffcyNk7w== + +"@xterm/addon-search@0.16.0-beta.21": + version "0.16.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.21.tgz#b8a20e83c1ff24afa675c3723244b2068255688d" + integrity sha512-RVn8yRx+w6R7abWiIttyAR0+Myh+XCYOLAkwco3iIYgzlztmox3Qp6YNzWJj0G8iwSvzxaSu7Fbjbb2PXTOSIg== + +"@xterm/addon-serialize@0.14.0-beta.21": + version "0.14.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.21.tgz#a074c5fdd2105c07574e6848babefef2905d84cb" + integrity sha512-Eg1QT2WG0pAIV+RrPv921+dVQvQqMhoFv2DQfMYDcqNbD2mTvIbX/ecEMb1bmn3WI0jNNomQ8UHZRFNRbDA+BA== + +"@xterm/addon-unicode11@0.9.0-beta.21": + version "0.9.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.21.tgz#dc843df701e518bc459e77dcd4fd65fe49adbb4b" + integrity sha512-IiHYZ+88m5MCoAyOHWQ4xXzecOh6FsDDr8lZpJktbFHyzYjBlIDQ6z9cJg+3ApApfo5Xosnmzjs27kf7wG2L0w== + +"@xterm/addon-webgl@0.19.0-beta.21": + version "0.19.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.21.tgz#68b92a47bf6768babd57bfbaf3ac97a7c670d8df" + integrity sha512-YV8Aaxp4QokXXehSCJ7NvudZKPDyBiXv4HqENqDpQllCj4hOWC5xJYSoFoPtu5+UhlzfqqvYRX/Il7QegPFPDg== + +"@xterm/headless@5.6.0-beta.21": + version "5.6.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.6.0-beta.21.tgz#110fa33b59f4bf2d1de188e318bb944c8d774e97" + integrity sha512-RtKsv7KZb/ee8hwkvMNYuUofDoBR/KWUjoB5mo10C+dHyDJcMYiG2k48cAvcaJRjPH721iOELORKQk3NAlowkg== + +"@xterm/xterm@5.6.0-beta.21": + version "5.6.0-beta.21" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.21.tgz#87a4e45752e5708cffc5c583d7f15e107313eb4e" + integrity sha512-1tLJaGudNSg1hEC+ZwUU7PiUvzURzKB5v1IRaJdmZK81ZCxvEF6Qfo281pTZsZFnv2iOWqFEC0C5uRmBXLm0lQ== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -2798,11 +2798,11 @@ braces@^2.3.1, braces@^2.3.2: to-regex "^3.0.1" braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browser-stdout@1.3.1: version "1.3.1" @@ -4944,10 +4944,10 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -5549,11 +5549,6 @@ got@^11.8.5: p-cancelable "^2.0.0" responselike "^2.0.0" -graceful-fs@4.2.11, graceful-fs@^4.2.11: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" @@ -5564,6 +5559,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.2.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== +graceful-fs@^4.2.11: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -6816,6 +6816,11 @@ js-base64@^3.7.4: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.4.tgz#af95b20f23efc8034afd2d1cc5b9d0adf7419037" integrity sha512-wpM/wi20Tl+3ifTyi0RdDckS4YTD4Lf953mBRrpG8547T7hInHNPEj8+ck4gB8VDcGyeAWFK++Wb/fU1BeavKQ== +js-base64@^3.7.5: + version "3.7.7" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" + integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== + js-beautify@^1.8.9: version "1.8.9" resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.8.9.tgz#08e3c05ead3ecfbd4f512c3895b1cda76c87d523" @@ -6852,10 +6857,10 @@ js-yaml@^3.13.0: argparse "^1.0.7" esprima "^4.0.0" -jschardet@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" - integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== +jschardet@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.1.2.tgz#9bf4364deba0677fe9e3bd9e29eda57febf2e9db" + integrity sha512-mw3CBZGzW8nUBPYhFU2ztZ/kJ6NClQUQVpyzvFMfznZsoC///ZQ30J2RCUanNsr5yF22LqhgYr/lj807/ZleWA== jsdoc-type-pratt-parser@~4.0.0: version "4.0.0" @@ -10158,7 +10163,7 @@ streamx@^2.15.0: fast-fifo "^1.1.0" queue-tick "^1.0.1" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10202,6 +10207,15 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -10297,7 +10311,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10332,6 +10346,13 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -10983,10 +11004,10 @@ typescript@^4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== -typescript@^5.5.0-dev.20240521: - version "5.5.0-dev.20240521" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.0-dev.20240521.tgz#a53f71ad2f5e4c4401a56c35993474b77813364c" - integrity sha512-52WLKX9mbRmStK1lb30KM78dSo5ssgQT8WQERYiv8JihXir4HUgwlgTz4crExojzpsGjFGFJROL/bZrhXUiOEQ== +typescript@^5.6.0-dev.20240618: + version "5.6.0-dev.20240618" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.0-dev.20240618.tgz#5ce1d67e5c9e36585349916a85a3f3d8cc806168" + integrity sha512-nUnATyFjcoenJB7S5oPGea2s0dd8MVl+2NisBLm7E+zpXkX0KSLy8Y7aFNSQ+r1Hs/MrKfSlV4O8yiQpCuOqrQ== typical@^4.0.0: version "4.0.0" @@ -11619,7 +11640,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11654,6 +11675,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -11676,9 +11706,9 @@ write@1.0.3: mkdirp "^0.5.1" ws@^7.2.0: - version "7.4.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" - integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== xml2js@^0.4.19: version "0.4.23" From b189a8b9c77a1fcc8a79732bfe24b2fa018436cc Mon Sep 17 00:00:00 2001 From: "Jennifer (Jenny) Bryan" Date: Tue, 16 Jul 2024 18:13:31 -0700 Subject: [PATCH 08/38] Prepare the `rmarkdown::render()` code snippet for the challenge of PowerShell (#3909) Addresses #3816 by using `vscode.ProcessExecution` instead of `vscode.ShellExecution`, to bypass some tricky quoting issues presented by PowerShell, without doing our own quoting or writing (much) OS- or shell-specific code. The problem is that we want to execute a code snippet like `rmarkdown::render("whatever.Rmd")` (where `"whatever.Rmd"` needs to be interpolated in, which is really neither here nor there). So you need quotes around this file path. But using `ShellExecution` you also need quotes around the whole `rmarkdown::render()` snippet; let's say these are single quotes. And, when executed in PowerShell, the entire command _also_ gets double quoted, which then changes the context for the quotes in the code snippet, leading to much misery: ``` The terminal process "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Command & 'C:\Program Files\R\R-4.3.3\bin\x64\R.exe' -e 'rmarkdown::render("d:\Users\jenny\rmd-render- fun\test.Rmd")'" terminated with exit code: 1. ``` By the time you get to R, the double quotes around the file path have gone missing (since I captured this error, I have changed the path separators here as well; but also that is not the problem): ``` > rmarkdown::render(d:\Users\jenny\rmd-render-fun\test.Rmd) Error: unexpected symbol in "rmarkdown::render(d:\Users" Execution halted ``` I tried all sorts of things, trying to stick with `vscode.ShellExecution` and `vscode.ShellQuoting` but never succeeded in finding a combination of argument strings and quoting that worked on Windows (PowerShell) and macOS. I became increasingly convinced, similar to this issue https://github.com/microsoft/vscode/issues/187661, that it might be impossible. ### QA Notes Exercise these commands on multiple OSes: * *R: Render Document with R Markdown*, find this in the command palette * *R: Source R File*, find this in the command palette or use the "play" button Here's a repo I used for experimentation while working on this PR: https://github.com/jennybc/rmd-render-fun. You could grab it with ```r usethis::create_from_github("https://github.com/jennybc/rmd-render-fun", destdir = "~/tmp") ``` Substitute your favorite destination directory for experiments in `destdir`. This folder sports 2 `.Rmd` files, with and without spaces in the filename, and likewise for `.R`. --- extensions/positron-r/src/commands.ts | 14 ++++---- extensions/positron-r/src/tasks.ts | 50 +++++++++++++++++++-------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/extensions/positron-r/src/commands.ts b/extensions/positron-r/src/commands.ts index 620f6d2b9e5..917b26d156e 100644 --- a/extensions/positron-r/src/commands.ts +++ b/extensions/positron-r/src/commands.ts @@ -147,8 +147,10 @@ export async function registerCommands(context: vscode.ExtensionContext) { vscode.commands.registerCommand('r.sourceCurrentFile', async () => { try { const filePath = await getEditorFilePathForCommand(); + // In the future, we may want to shorten the path by making it + // relative to the current working directory. if (filePath) { - const command = `source(${filePath})`; + const command = `source(${JSON.stringify(filePath)})`; positron.runtime.executeCode('r', command, false); } } catch (e) { @@ -324,14 +326,10 @@ export async function getEditorFilePathForCommand() { // the VS Code file system API. const fsStat = await vscode.workspace.fs.stat(vscode.Uri.file(filePath)); - // In the future, we will want to shorten the path by making it - // relative to the current directory; doing so, however, will - // require the kernel to alert us to the current working directory. - // - // For now, just use the full path, passed through JSON encoding - // to ensure that it is properly escaped. + // Return the full path, with POSIX path separators. Any additional path + // math, escaping, or quoting is the responsibility of the caller. if (fsStat) { - return JSON.stringify(filePath); + return filePath.replace(/\\/g, '/'); } return; } diff --git a/extensions/positron-r/src/tasks.ts b/extensions/positron-r/src/tasks.ts index 91b0afb8fb9..f5532e00407 100644 --- a/extensions/positron-r/src/tasks.ts +++ b/extensions/positron-r/src/tasks.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import * as os from 'os'; import { RSessionManager } from './session-manager'; -import { getEditorFilePathForCommand } from './commands'; import { getPandocPath } from './pandoc'; export class RPackageTaskProvider implements vscode.TaskProvider { @@ -54,7 +54,7 @@ export async function getRPackageTasks(editorFilePath?: string): Promise new vscode.Task( - { type: 'rPackageTask', task: data.task, pkg: data.package }, - vscode.TaskScope.Workspace, - data.message, - 'R', - new vscode.ShellExecution( - binpath, - ['-e', { value: data.rcode, quoting: vscode.ShellQuoting.Strong }], - { env } - ), - [] - )); + return taskData.map(data => { + let exec: vscode.ProcessExecution | vscode.ShellExecution; + if (data.task === 'r.task.rmarkdownRender' && os.platform() === 'win32') { + // Using vscode.ProcessExecution gets around some hairy quoting issues on Windows, + // specifically encountered with PowerShell. + // https://github.com/posit-dev/positron/issues/3816 + // We don't know of specific problems around not using a shell (for example, env vars + // appear to be inherited by ProcessExecution), but we're still scoping this narrowly + // out of caution. + exec = new vscode.ProcessExecution( + binpath, + ['-e', data.rcode], + { env } + ); + } else { + // The explicit quoting treatment here is also motivated by PowerShell, so make sure to + // test any changes on Windows. + exec = new vscode.ShellExecution( + binpath, + ['-e', { value: data.rcode, quoting: vscode.ShellQuoting.Strong }], + { env } + ); + } + + return new vscode.Task( + { type: 'rPackageTask', task: data.task, pkg: data.package }, + vscode.TaskScope.Workspace, + data.message, + 'R', + exec, + [] + ); + }); } From 774ad276b0d96d88f8b1946f96970e7f5bccea53 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 16 Jul 2024 19:51:27 -0700 Subject: [PATCH 09/38] Run show-version scripts in node explicitly (#4039) This change fixes an annoying issue in Windows dev environments that can cause Visual Studio to open during the incremental build. The problem is that Gulp is invoking `show-version.js`. On Unix-alikes, the shebang comment in this file causes it to run under Node, but on Windows, invoking the file opens it in the default editor for `.js` files. The fix is to use Node explicitly instead of relying on the shebang. ### QA Notes N/A, build change only. --- build/gulpfile.vscode.js | 2 +- build/gulpfile.vscode.web.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 263bc6a21e2..bfd584479ad 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -253,7 +253,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op const positronBuildNumber = process.env.POSITRON_BUILD_NUMBER ?? child_process.execSync( - `${path.dirname(__dirname)}/versions/show-version.js --build`).toString().trim(); + `node ${path.dirname(__dirname)}/versions/show-version.js --build`).toString().trim(); // --- End Positron --- diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index 0ca23fd8f75..8123c3a291a 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -88,7 +88,7 @@ const buildDate = new Date().toISOString(); // Use the POSITRON_BUILD_NUMBER var if it's set; otherwise, call show-version to compute it. const buildNumber = process.env.POSITRON_BUILD_NUMBER ?? - child_process.execSync(`${REPO_ROOT}/versions/show-version.js --build`).toString().trim(); + child_process.execSync(`node ${REPO_ROOT}/versions/show-version.js --build`).toString().trim(); // --- End Positron --- /** From 8db19e0e48305b1444c4e4146de943e2bd189273 Mon Sep 17 00:00:00 2001 From: Wasim Lorgat Date: Wed, 17 Jul 2024 12:21:04 +0200 Subject: [PATCH 10/38] Improvements to debouncing console execution indicators (#4027) Addresses #3752. I've also given all of our `@keyframes` unique names to avoid unintentionally overwriting keyframes across components, which broke debouncing the activity item to begin with. I also made the activity item indicator animation timing the same as the interrupt button, which I think feels better. Lastly, I added the same fade in to the action bar separator. Before this PR, it would show immediately even though the interrupt button next to it faded in. https://github.com/user-attachments/assets/1c24e135-1910-4f0d-907d-06dab8dbf142 #### QA Notes It's worth playing around with different Positron components that animate to make sure the renames didn't break anything. --- .../browser/components/actionBarButton.css | 4 ++-- .../browser/components/actionBarFilter.css | 4 ++-- .../browser/components/actionBarFind.css | 4 ++-- .../browser/components/actionBarSeparator.css | 15 +++++++++++++++ .../browser/components/actionBarSeparator.tsx | 17 +++++++++++++++-- .../positronAnsiRenderer/outputRun.css | 2 +- .../positronAnsiRenderer/outputRun.tsx | 4 ++-- .../positronModalDialog.css | 8 ++++---- .../components/columnSearch.css | 4 ++-- .../components/statusBarActivityIndicator.css | 4 ++-- .../positronModalReactRenderer.css | 4 ++-- .../browser/components/actionBar.tsx | 2 +- .../browser/components/activityInput.css | 19 +++++-------------- .../browser/components/variableItem.css | 8 ++++---- 14 files changed, 59 insertions(+), 40 deletions(-) diff --git a/src/vs/platform/positronActionBar/browser/components/actionBarButton.css b/src/vs/platform/positronActionBar/browser/components/actionBarButton.css index 4db990042c2..ff27c1ae62b 100644 --- a/src/vs/platform/positronActionBar/browser/components/actionBarButton.css +++ b/src/vs/platform/positronActionBar/browser/components/actionBarButton.css @@ -3,7 +3,7 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -@keyframes fade-in { +@keyframes positronActionBarButton-fadeIn { 0% { opacity: 0; } @@ -36,7 +36,7 @@ .action-bar-button.fade-in { opacity: 0; - animation: fade-in 0.25s ease-in 0.25s 1 forwards; + animation: positronActionBarButton-fadeIn 0.25s ease-in 0.25s 1 forwards; } .action-bar-button:focus { diff --git a/src/vs/platform/positronActionBar/browser/components/actionBarFilter.css b/src/vs/platform/positronActionBar/browser/components/actionBarFilter.css index 2279081c7fc..4227e6b6a80 100644 --- a/src/vs/platform/positronActionBar/browser/components/actionBarFilter.css +++ b/src/vs/platform/positronActionBar/browser/components/actionBarFilter.css @@ -3,7 +3,7 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -@keyframes fadeIn { +@keyframes positronActionBarFilter-fadeIn { from { opacity: 0; } to { opacity: 1; } } @@ -57,7 +57,7 @@ cursor: pointer; margin: 0 4px 0 0; background: transparent; - animation: fadeIn 150ms ease-out; + animation: positronActionBarFilter-fadeIn 150ms ease-out; } .action-bar-filter-input .clear-button:focus-visible { diff --git a/src/vs/platform/positronActionBar/browser/components/actionBarFind.css b/src/vs/platform/positronActionBar/browser/components/actionBarFind.css index 52ea06d9c97..a67895cad8e 100644 --- a/src/vs/platform/positronActionBar/browser/components/actionBarFind.css +++ b/src/vs/platform/positronActionBar/browser/components/actionBarFind.css @@ -3,7 +3,7 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ -@keyframes fadeIn { +@keyframes positronActionBarFind-fadeIn { from { opacity: 0; } to { opacity: 1; } } @@ -57,7 +57,7 @@ cursor: pointer; margin: 0 4px 0 0; background: transparent; - animation: fadeIn 150ms ease-out; + animation: positronActionBarFind-fadeIn 150ms ease-out; } .action-bar-find-input .clear-button:focus-visible { diff --git a/src/vs/platform/positronActionBar/browser/components/actionBarSeparator.css b/src/vs/platform/positronActionBar/browser/components/actionBarSeparator.css index 84765b3a4c4..785dec50d33 100644 --- a/src/vs/platform/positronActionBar/browser/components/actionBarSeparator.css +++ b/src/vs/platform/positronActionBar/browser/components/actionBarSeparator.css @@ -3,6 +3,16 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ +@keyframes positronActionBarSeparator-fadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + .action-bar-separator { width: 7px; align-items: center; @@ -10,6 +20,11 @@ display: flex !important; } +.action-bar-separator.fade-in { + opacity: 0; + animation: positronActionBarSeparator-fadeIn 0.25s ease-in 0.25s 1 forwards; +} + .action-bar-separator-icon { align-items: center; justify-content: center; diff --git a/src/vs/platform/positronActionBar/browser/components/actionBarSeparator.tsx b/src/vs/platform/positronActionBar/browser/components/actionBarSeparator.tsx index cc83faa8a9c..8c2f784e8c4 100644 --- a/src/vs/platform/positronActionBar/browser/components/actionBarSeparator.tsx +++ b/src/vs/platform/positronActionBar/browser/components/actionBarSeparator.tsx @@ -5,15 +5,28 @@ import 'vs/css!./actionBarSeparator'; import * as React from 'react'; +import { optionalBoolean, positronClassNames } from 'vs/base/common/positronUtilities'; + +/** + * ActionBarSeparatorProps interface. + */ +export interface ActionBarSeparatorProps { + fadeIn?: boolean; +} /** * ActionBarSeparator component. * @returns The component. */ -export const ActionBarSeparator = () => { +export const ActionBarSeparator = (props: ActionBarSeparatorProps) => { // Render. return ( -