diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f17fa843645..2a019ed7205 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,7 +93,7 @@ Once submitted, your report will go into the [issue tracking](https://github.com We use GitHub Actions to help us manage issues. These Actions and their descriptions can be [viewed here](https://github.com/microsoft/vscode-github-triage-actions). Some examples of what these Actions do are: -* Automatically closes any issue marked `needs-more-info` if there has been no response in the past 7 days. +* Automatically closes any issue marked `info-needed` if there has been no response in the past 7 days. * Automatically lock issues 45 days after they are closed. * Automatically implement the VS Code [feature request pipeline](https://github.com/microsoft/vscode/wiki/Issues-Triaging#managing-feature-requests). diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index d1bee378ef2..7863635ae99 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -225,6 +225,9 @@ pub struct CommandShellArgs { /// Require the given token string to be given in the handshake. #[clap(long)] pub require_token: Option, + /// Optional parent process id. If provided, the server will be stopped when the process of the given pid no longer exists + #[clap(long, hide = true)] + pub parent_process_id: Option, } #[derive(Args, Debug, Clone)] diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index e9ca2277d9f..c0b3f3f01f9 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -136,6 +136,11 @@ impl ServiceContainer for TunnelServiceContainer { pub async fn command_shell(ctx: CommandContext, args: CommandShellArgs) -> Result { let platform = PreReqChecker::new().verify().await?; + let mut shutdown_reqs = vec![ShutdownRequest::CtrlC]; + if let Some(p) = args.parent_process_id.and_then(|p| Pid::from_str(&p).ok()) { + shutdown_reqs.push(ShutdownRequest::ParentProcessKilled(p)); + } + let mut params = ServeStreamParams { log: ctx.log, launcher_paths: ctx.paths, @@ -144,7 +149,7 @@ pub async fn command_shell(ctx: CommandContext, args: CommandShellArgs) -> Resul .require_token .map(AuthRequired::VSDAWithToken) .unwrap_or(AuthRequired::VSDA), - exit_barrier: ShutdownRequest::create_rx([ShutdownRequest::CtrlC]), + exit_barrier: ShutdownRequest::create_rx(shutdown_reqs), code_server_args: (&ctx.args).into(), }; diff --git a/cli/src/commands/update.rs b/cli/src/commands/update.rs index 0d7321a814f..30c918f7e85 100644 --- a/cli/src/commands/update.rs +++ b/cli/src/commands/update.rs @@ -23,6 +23,8 @@ pub async fn update(ctx: CommandContext, args: StandaloneUpdateArgs) -> Result { update_service: &'a UpdateService, } +static OLD_UPDATE_EXTENSION: &str = "Updating CLI"; + impl<'a> SelfUpdate<'a> { pub fn new(update_service: &'a UpdateService) -> Result { let commit = VSCODE_CLI_COMMIT @@ -59,6 +61,18 @@ impl<'a> SelfUpdate<'a> { release.commit == self.commit } + /// Cleans up old self-updated binaries. Should be called with regularity. + /// May fail if old versions are still running. + pub fn cleanup_old_update(&self) -> Result<(), std::io::Error> { + let current_path = std::env::current_exe()?; + let old_path = current_path.with_extension(OLD_UPDATE_EXTENSION); + if old_path.exists() { + fs::remove_file(old_path)?; + } + + Ok(()) + } + /// Updates the CLI to the given release. pub async fn do_update( &self, @@ -89,8 +103,11 @@ impl<'a> SelfUpdate<'a> { // OS later. However, this can fail if the tempdir is on a different drive // than the installation dir. In this case just rename it to ".old". if fs::rename(&target_path, tempdir.path().join("old-code-cli")).is_err() { - fs::rename(&target_path, target_path.with_extension(".old")) - .map_err(|e| wrap(e, "failed to rename old CLI"))?; + fs::rename( + &target_path, + target_path.with_extension(OLD_UPDATE_EXTENSION), + ) + .map_err(|e| wrap(e, "failed to rename old CLI"))?; } fs::rename(&staging_path, &target_path) diff --git a/cli/src/tunnels/control_server.rs b/cli/src/tunnels/control_server.rs index af0605bb66d..a344cc5d560 100644 --- a/cli/src/tunnels/control_server.rs +++ b/cli/src/tunnels/control_server.rs @@ -777,6 +777,8 @@ async fn handle_update( let latest_release = updater.get_current_release().await?; let up_to_date = updater.is_up_to_date_with(&latest_release); + let _ = updater.cleanup_old_update(); + if !params.do_update || up_to_date { return Ok(UpdateResult { up_to_date, diff --git a/cli/src/util/tar.rs b/cli/src/util/tar.rs index 248f63f9720..0a5496411f7 100644 --- a/cli/src/util/tar.rs +++ b/cli/src/util/tar.rs @@ -10,6 +10,7 @@ use std::io::Seek; use std::path::{Path, PathBuf}; use tar::Archive; +use super::errors::wrapdbg; use super::io::ReportCopyProgress; fn should_skip_first_segment(file: &fs::File) -> Result { @@ -93,7 +94,7 @@ where entry .unpack(&path) - .map_err(|e| wrap(e, format!("error unpacking {}", path.display())))?; + .map_err(|e| wrapdbg(e, format!("error unpacking {}", path.display())))?; Ok(path) }) .collect::, WrappedError>>()?; diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index f5a5b27b93c..df928afdfc7 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -122,7 +122,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec }); // History item change decoration - const fileDecoration = this.historyItemChangeFileDecoration(change.status); + const fileDecoration = this.getHistoryItemChangeFileDecoration(change.status); this.historyItemDecorations.set(historyItemUri.toString(), fileDecoration); historyItemChangesUri.push(historyItemUri); @@ -161,15 +161,12 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return this.historyItemDecorations.get(uri.toString()); } - private historyItemChangeFileDecoration(status: Status): FileDecoration { + private getHistoryItemChangeFileDecoration(status: Status): FileDecoration { const letter = Resource.getStatusLetter(status); const tooltip = Resource.getStatusText(status); const color = Resource.getStatusColor(status); - const fileDecoration = new FileDecoration(letter, tooltip, color); - fileDecoration.propagate = status !== Status.DELETED && status !== Status.INDEX_DELETED; - - return fileDecoration; + return new FileDecoration(letter, tooltip, color); } private async getSummaryHistoryItem(ref1: string, ref2: string): Promise { diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 5e72873b3ef..c87d40fe51c 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -524,13 +524,6 @@ export class AzureActiveDirectoryService { throw e; } - let label; - if (claims.name && claims.email) { - label = `${claims.name} - ${claims.email}`; - } else { - label = claims.email ?? claims.unique_name ?? claims.preferred_username ?? 'user@example.com'; - } - const id = `${claims.tid}/${(claims.oid ?? (claims.altsecid ?? '' + claims.ipd ?? ''))}`; const sessionId = existingId || `${id}/${randomUUID()}`; this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' Token response parsed successfully.`); @@ -543,7 +536,7 @@ export class AzureActiveDirectoryService { scope: scopeData.scopeStr, sessionId, account: { - label, + label: claims.email ?? claims.preferred_username ?? claims.unique_name ?? 'user@example.com', id, type: claims.tid === MSA_TID || claims.tid === MSA_PASSTHRU_TID ? MicrosoftAccountType.MSA : MicrosoftAccountType.AAD } diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 4275898e244..27e25dbf17f 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -387,7 +387,8 @@ import { assertNoRpc, poll } from '../utils'; }); suite('window.onDidWriteTerminalData', () => { - test('should listen to all future terminal data events', function (done) { + // still flaky with retries, skipping https://github.com/microsoft/vscode/issues/193505 + test.skip('should listen to all future terminal data events', function (done) { // This test has been flaky in the past but it's not clear why, possibly because // events from previous tests polluting the event recording in this test. Retries // was added so we continue to have coverage of the onDidWriteTerminalData API. diff --git a/package.json b/package.json index e4e125925a7..48f132b44ef 100644 --- a/package.json +++ b/package.json @@ -219,7 +219,7 @@ "webpack-cli": "^5.0.1", "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", - "yaserver": "^0.2.0" + "yaserver": "^0.4.0" }, "repository": { "type": "git", diff --git a/src/main.js b/src/main.js index 55200575103..3c0a6530e7c 100644 --- a/src/main.js +++ b/src/main.js @@ -278,7 +278,9 @@ function configureCommandlineSwitchesSync(cliArgs) { // Following features are disabled from the runtime: // `CalculateNativeWinOcclusion` - Disable native window occlusion tracker (https://groups.google.com/a/chromium.org/g/embedder-dev/c/ZF3uHHyWLKw/m/VDN2hDXMAAAJ) - app.commandLine.appendSwitch('disable-features', 'CalculateNativeWinOcclusion'); + const featuresToDisable = + `CalculateNativeWinOcclusion,${app.commandLine.getSwitchValue('disable-features')}`; + app.commandLine.appendSwitch('disable-features', featuresToDisable); // Support JS Flags const jsFlags = getJSFlags(cliArgs); diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index 2a77409cd83..a12d3fb4515 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -7,6 +7,7 @@ import { IDragAndDropData } from 'vs/base/browser/dnd'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { GestureEvent } from 'vs/base/browser/touch'; +import { IDisposable } from 'vs/base/common/lifecycle'; export interface IListVirtualDelegate { getHeight(element: T): number; @@ -99,7 +100,11 @@ export const ListDragOverReactions = { accept(): IListDragOverReaction { return { accept: true }; }, }; -export interface IListDragAndDrop { +/** + * Warning: Once passed to a list, that list takes up + * the responsibility of disposing it. + */ +export interface IListDragAndDrop extends IDisposable { getDragURI(element: T): string | null; getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined; onDragStart?(data: IDragAndDropData, originalEvent: DragEvent): void; diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index df252f81290..9c041bd77f6 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -13,7 +13,7 @@ import { distinct, equals } from 'vs/base/common/arrays'; import { Delayer, disposableTimeout } from 'vs/base/common/async'; import { memoize } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IRange, Range } from 'vs/base/common/range'; import { INewScrollDimensions, Scrollable, ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable'; import { ISpliceable } from 'vs/base/common/sequence'; @@ -90,7 +90,8 @@ const DefaultOptions = { getDragURI() { return null; }, onDragStart(): void { }, onDragOver() { return false; }, - drop() { } + drop() { }, + dispose() { } }, horizontalScrolling: false, transformOptimization: true, @@ -436,7 +437,7 @@ export class ListView implements IListView { this.setRowLineHeight = options.setRowLineHeight ?? DefaultOptions.setRowLineHeight; this.setRowHeight = options.setRowHeight ?? DefaultOptions.setRowHeight; this.supportDynamicHeights = options.supportDynamicHeights ?? DefaultOptions.supportDynamicHeights; - this.dnd = options.dnd ?? DefaultOptions.dnd; + this.dnd = options.dnd ?? this.disposables.add(DefaultOptions.dnd); this.layout(options.initialSize?.height, options.initialSize?.width); } @@ -1241,7 +1242,7 @@ export class ListView implements IListView { private onDragLeave(event: IListDragEvent): void { this.onDragLeaveTimeout.dispose(); - this.onDragLeaveTimeout = disposableTimeout(() => this.clearDragOverFeedback(), 100); + this.onDragLeaveTimeout = disposableTimeout(() => this.clearDragOverFeedback(), 100, this.disposables); if (this.currentDragData) { this.dnd.onDragLeave?.(this.currentDragData, event.element, event.index, event.browserEvent); } @@ -1299,7 +1300,7 @@ export class ListView implements IListView { this.dragOverAnimationDisposable.dispose(); this.dragOverAnimationDisposable = undefined; } - }, 1000); + }, 1000, this.disposables); this.dragOverMouseY = event.pageY; } @@ -1544,6 +1545,7 @@ export class ListView implements IListView { this.domNode.parentNode.removeChild(this.domNode); } - dispose(this.disposables); + this.dragOverAnimationDisposable?.dispose(); + this.disposables.dispose(); } } diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 1bd342c99c7..3bb50780f93 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -1097,7 +1097,8 @@ const DefaultOptions: IListOptions = { getDragURI() { return null; }, onDragStart(): void { }, onDragOver() { return false; }, - drop() { } + drop() { }, + dispose() { } } }; @@ -1298,6 +1299,10 @@ class ListViewDragAndDrop implements IListViewDragAndDrop { drop(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void { this.dnd.drop(data, targetElement, targetIndex, originalEvent); } + + dispose(): void { + this.dnd.dispose(); + } } /** diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 07a22c43e91..3c7aa3ad308 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -61,6 +61,7 @@ class TreeNodeListDragAndDrop implements IListDragAndDrop< private autoExpandNode: ITreeNode | undefined; private autoExpandDisposable: IDisposable = Disposable.None; + private disposables = new DisposableStore(); constructor(private modelProvider: () => ITreeModel, private dnd: ITreeDragAndDrop) { } @@ -103,7 +104,7 @@ class TreeNodeListDragAndDrop implements IListDragAndDrop< } this.autoExpandNode = undefined; - }, 500); + }, 500, this.disposables); } if (typeof result === 'boolean' || !result.accept || typeof result.bubble === 'undefined' || result.feedback) { @@ -144,6 +145,11 @@ class TreeNodeListDragAndDrop implements IListDragAndDrop< onDragEnd(originalEvent: DragEvent): void { this.dnd.onDragEnd?.(originalEvent); } + + dispose(): void { + this.disposables.dispose(); + this.dnd.dispose(); + } } function asListOptions(modelProvider: () => ITreeModel, options?: IAbstractTreeOptions): IListOptions> | undefined { diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index ad460ab3d1d..5ba79157f2d 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -208,6 +208,10 @@ class AsyncDataTreeNodeListDragAndDrop implements IListDragAndDrop(options?: IAsyncDataTreeOptions): IObjectTreeOptions, TFilterData> | undefined { diff --git a/src/vs/base/common/collections.ts b/src/vs/base/common/collections.ts index 8fdf74b05be..4ff9b49829c 100644 --- a/src/vs/base/common/collections.ts +++ b/src/vs/base/common/collections.ts @@ -63,6 +63,24 @@ export function diffMaps(before: Map, after: Map): { removed: } return { removed, added }; } + +/** + * Computes the intersection of two sets. + * + * @param setA - The first set. + * @param setB - The second iterable. + * @returns A new set containing the elements that are in both `setA` and `setB`. + */ +export function intersection(setA: Set, setB: Iterable): Set { + const result = new Set(); + for (const elem of setB) { + if (setA.has(elem)) { + result.add(elem); + } + } + return result; +} + export class SetMap { private map = new Map>(); diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index b8e1da4b777..8bf0cc71a30 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { once as onceFn } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; import { IObservable, IObserver } from 'vs/base/common/observable'; @@ -1449,7 +1449,7 @@ export class EventMultiplexer implements IDisposable { this.events.splice(idx, 1); }; - return toDisposable(onceFn(dispose)); + return toDisposable(createSingleCallFunction(dispose)); } private onFirstListenerAdd(): void { diff --git a/src/vs/base/common/functional.ts b/src/vs/base/common/functional.ts index b437cc98c46..a70a748e08c 100644 --- a/src/vs/base/common/functional.ts +++ b/src/vs/base/common/functional.ts @@ -3,7 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export function once(this: unknown, fn: T): T { +/** + * Given a function, returns a function that is only calling that function once. + */ +export function createSingleCallFunction(this: unknown, fn: T): T { const _this = this; let didCall = false; let result: unknown; diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index de7b5d15e6e..a1aa7f2bd66 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -5,7 +5,7 @@ import { compareBy, numberComparator } from 'vs/base/common/arrays'; import { SetMap, groupBy } from 'vs/base/common/collections'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { Iterable } from 'vs/base/common/iterator'; // #region Disposable Tracking @@ -345,7 +345,7 @@ export function combinedDisposable(...disposables: IDisposable[]): IDisposable { */ export function toDisposable(fn: () => void): IDisposable { const self = trackDisposable({ - dispose: once(() => { + dispose: createSingleCallFunction(() => { markAsDisposed(self); fn(); }) @@ -623,7 +623,7 @@ export abstract class ReferenceCollection { } const { object } = reference; - const dispose = once(() => { + const dispose = createSingleCallFunction(() => { if (--reference!.counter === 0) { this.destroyReferencedObject(key, reference!.object); this.references.delete(key); diff --git a/src/vs/base/node/crypto.ts b/src/vs/base/node/crypto.ts index 8a2ecc9e250..7631aac0524 100644 --- a/src/vs/base/node/crypto.ts +++ b/src/vs/base/node/crypto.ts @@ -5,7 +5,7 @@ import * as crypto from 'crypto'; import * as fs from 'fs'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; export async function checksum(path: string, sha1hash: string | undefined): Promise { const checksumPromise = new Promise((resolve, reject) => { @@ -13,7 +13,7 @@ export async function checksum(path: string, sha1hash: string | undefined): Prom const hash = crypto.createHash('sha1'); input.pipe(hash); - const done = once((err?: Error, result?: string) => { + const done = createSingleCallFunction((err?: Error, result?: string) => { input.removeAllListeners(); hash.removeAllListeners(); diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts index bd1382b36e2..1da026b5174 100644 --- a/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -10,6 +10,7 @@ import { marked } from 'vs/base/common/marked/marked'; import { parse } from 'vs/base/common/marshalling'; import { isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; function strToNode(str: string): HTMLElement { return new DOMParser().parseFromString(str, 'text/html').body.firstChild as HTMLElement; @@ -23,10 +24,13 @@ function assertNodeEquals(actualNode: HTMLElement, expectedHtml: string) { } suite('MarkdownRenderer', () => { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + suite('Sanitization', () => { test('Should not render images with unknown schemes', () => { const markdown = { value: `![image](no-such://example.com/cat.gif)` }; - const result: HTMLElement = renderMarkdown(markdown).element; + const result: HTMLElement = store.add(renderMarkdown(markdown)).element; assert.strictEqual(result.innerHTML, '

image

'); }); }); @@ -34,28 +38,28 @@ suite('MarkdownRenderer', () => { suite('Images', () => { test('image rendering conforms to default', () => { const markdown = { value: `![image](http://example.com/cat.gif 'caption')` }; - const result: HTMLElement = renderMarkdown(markdown).element; + const result: HTMLElement = store.add(renderMarkdown(markdown)).element; assertNodeEquals(result, '

image

'); }); test('image rendering conforms to default without title', () => { const markdown = { value: `![image](http://example.com/cat.gif)` }; - const result: HTMLElement = renderMarkdown(markdown).element; + const result: HTMLElement = store.add(renderMarkdown(markdown)).element; assertNodeEquals(result, '

image

'); }); test('image width from title params', () => { - const result: HTMLElement = renderMarkdown({ value: `![image](http://example.com/cat.gif|width=100px 'caption')` }).element; + const result: HTMLElement = store.add(renderMarkdown({ value: `![image](http://example.com/cat.gif|width=100px 'caption')` })).element; assertNodeEquals(result, `

image

`); }); test('image height from title params', () => { - const result: HTMLElement = renderMarkdown({ value: `![image](http://example.com/cat.gif|height=100 'caption')` }).element; + const result: HTMLElement = store.add(renderMarkdown({ value: `![image](http://example.com/cat.gif|height=100 'caption')` })).element; assertNodeEquals(result, `

image

`); }); test('image width and height from title params', () => { - const result: HTMLElement = renderMarkdown({ value: `![image](http://example.com/cat.gif|height=200,width=100 'caption')` }).element; + const result: HTMLElement = store.add(renderMarkdown({ value: `![image](http://example.com/cat.gif|height=200,width=100 'caption')` })).element; assertNodeEquals(result, `

image

`); }); @@ -63,7 +67,7 @@ suite('MarkdownRenderer', () => { if (isWeb) { return; } - const result: HTMLElement = renderMarkdown({ value: `![image](file:///images/cat.gif)` }).element; + const result: HTMLElement = store.add(renderMarkdown({ value: `![image](file:///images/cat.gif)` })).element; assertNodeEquals(result, '

image

'); }); }); @@ -78,10 +82,10 @@ suite('MarkdownRenderer', () => { test('asyncRenderCallback should be invoked for code blocks', () => { const markdown = { value: '```js\n1 + 1;\n```' }; return new Promise(resolve => { - renderMarkdown(markdown, { + store.add(renderMarkdown(markdown, { asyncRenderCallback: resolve, codeBlockRenderer: simpleCodeBlockRenderer - }); + })); }); }); @@ -120,12 +124,12 @@ suite('MarkdownRenderer', () => { test('Code blocks should use leading language id (#157793)', async () => { const markdown = { value: '```js some other stuff\n1 + 1;\n```' }; const lang = await new Promise(resolve => { - renderMarkdown(markdown, { + store.add(renderMarkdown(markdown, { codeBlockRenderer: async (lang, value) => { resolve(lang); return simpleCodeBlockRenderer(lang, value); } - }); + })); }); assert.strictEqual(lang, 'js'); }); @@ -137,7 +141,7 @@ suite('MarkdownRenderer', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendText('$(zap) $(not a theme icon) $(add)'); - const result: HTMLElement = renderMarkdown(mds).element; + const result: HTMLElement = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, `

$(zap) $(not a theme icon) $(add)

`); }); @@ -145,7 +149,7 @@ suite('MarkdownRenderer', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendMarkdown('$(zap) $(not a theme icon) $(add)'); - const result: HTMLElement = renderMarkdown(mds).element; + const result: HTMLElement = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, `

$(not a theme icon)

`); }); @@ -153,7 +157,7 @@ suite('MarkdownRenderer', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendMarkdown('\\$(zap) $(not a theme icon) $(add)'); - const result: HTMLElement = renderMarkdown(mds).element; + const result: HTMLElement = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, `

$(zap) $(not a theme icon)

`); }); @@ -161,7 +165,7 @@ suite('MarkdownRenderer', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendMarkdown(`[$(zap)-link](#link)`); - const result: HTMLElement = renderMarkdown(mds).element; + const result: HTMLElement = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, `

-link

`); }); @@ -172,7 +176,7 @@ suite('MarkdownRenderer', () => { |--------|----------------------| | $(zap) | [$(zap)-link](#link) |`); - const result: HTMLElement = renderMarkdown(mds).element; + const result: HTMLElement = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, ` @@ -192,7 +196,7 @@ suite('MarkdownRenderer', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true, supportHtml: true }); mds.appendMarkdown(`$(sync)`); - const result: HTMLElement = renderMarkdown(mds).element; + const result: HTMLElement = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, `

`); }); }); @@ -203,7 +207,7 @@ suite('MarkdownRenderer', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: false }); mds.appendText('$(zap) $(not a theme icon) $(add)'); - const result: HTMLElement = renderMarkdown(mds).element; + const result: HTMLElement = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, `

$(zap) $(not a theme icon) $(add)

`); }); @@ -211,7 +215,7 @@ suite('MarkdownRenderer', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: false }); mds.appendMarkdown('\\$(zap) $(not a theme icon) $(add)'); - const result: HTMLElement = renderMarkdown(mds).element; + const result: HTMLElement = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, `

$(zap) $(not a theme icon) $(add)

`); }); }); @@ -219,7 +223,7 @@ suite('MarkdownRenderer', () => { test('npm Hover Run Script not working #90855', function () { const md: IMarkdownString = JSON.parse('{"value":"[Run Script](command:npm.runScriptFromHover?%7B%22documentUri%22%3A%7B%22%24mid%22%3A1%2C%22fsPath%22%3A%22c%3A%5C%5CUsers%5C%5Cjrieken%5C%5CCode%5C%5C_sample%5C%5Cfoo%5C%5Cpackage.json%22%2C%22_sep%22%3A1%2C%22external%22%3A%22file%3A%2F%2F%2Fc%253A%2FUsers%2Fjrieken%2FCode%2F_sample%2Ffoo%2Fpackage.json%22%2C%22path%22%3A%22%2Fc%3A%2FUsers%2Fjrieken%2FCode%2F_sample%2Ffoo%2Fpackage.json%22%2C%22scheme%22%3A%22file%22%7D%2C%22script%22%3A%22echo%22%7D \\"Run the script as a task\\")","supportThemeIcons":false,"isTrusted":true,"uris":{"__uri_e49443":{"$mid":1,"fsPath":"c:\\\\Users\\\\jrieken\\\\Code\\\\_sample\\\\foo\\\\package.json","_sep":1,"external":"file:///c%3A/Users/jrieken/Code/_sample/foo/package.json","path":"/c:/Users/jrieken/Code/_sample/foo/package.json","scheme":"file"},"command:npm.runScriptFromHover?%7B%22documentUri%22%3A%7B%22%24mid%22%3A1%2C%22fsPath%22%3A%22c%3A%5C%5CUsers%5C%5Cjrieken%5C%5CCode%5C%5C_sample%5C%5Cfoo%5C%5Cpackage.json%22%2C%22_sep%22%3A1%2C%22external%22%3A%22file%3A%2F%2F%2Fc%253A%2FUsers%2Fjrieken%2FCode%2F_sample%2Ffoo%2Fpackage.json%22%2C%22path%22%3A%22%2Fc%3A%2FUsers%2Fjrieken%2FCode%2F_sample%2Ffoo%2Fpackage.json%22%2C%22scheme%22%3A%22file%22%7D%2C%22script%22%3A%22echo%22%7D":{"$mid":1,"path":"npm.runScriptFromHover","scheme":"command","query":"{\\"documentUri\\":\\"__uri_e49443\\",\\"script\\":\\"echo\\"}"}}}'); - const element = renderMarkdown(md).element; + const element = store.add(renderMarkdown(md)).element; const anchor = element.querySelector('a')!; assert.ok(anchor); @@ -238,7 +242,7 @@ suite('MarkdownRenderer', () => { supportHtml: true }); - const result: HTMLElement = renderMarkdown(md).element; + const result: HTMLElement = store.add(renderMarkdown(md)).element; assert.strictEqual(result.innerHTML, `

command1 command2

`); }); @@ -248,7 +252,7 @@ suite('MarkdownRenderer', () => { supportHtml: true, }); - const result: HTMLElement = renderMarkdown(md).element; + const result: HTMLElement = store.add(renderMarkdown(md)).element; assert.strictEqual(result.innerHTML, `

command1 command2

`); }); @@ -274,7 +278,7 @@ suite('MarkdownRenderer', () => { const mds = new MarkdownString(undefined, {}); mds.appendMarkdown('abc'); - const result = renderMarkdown(mds).element; + const result = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, `

abc

`); }); @@ -282,7 +286,7 @@ suite('MarkdownRenderer', () => { const mds = new MarkdownString(undefined, { supportHtml: true }); mds.appendMarkdown('abc'); - const result = renderMarkdown(mds).element; + const result = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, `

abc

`); }); @@ -290,7 +294,7 @@ suite('MarkdownRenderer', () => { const mds = new MarkdownString(undefined, { supportHtml: true }); mds.appendMarkdown('abc'); - const result = renderMarkdown(mds).element; + const result = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, `

abc

`); }); @@ -298,7 +302,7 @@ suite('MarkdownRenderer', () => { const mds = new MarkdownString(undefined, { supportHtml: true }); mds.appendText('abc'); - const result = renderMarkdown(mds).element; + const result = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, `

a<b>b</b>c

`); }); @@ -310,7 +314,7 @@ suite('MarkdownRenderer', () => { const mds = new MarkdownString(undefined, { supportHtml: true }); mds.appendMarkdown(``); - const result = renderMarkdown(mds).element; + const result = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, ``); }); @@ -322,7 +326,7 @@ suite('MarkdownRenderer', () => { const mds = new MarkdownString(undefined, { supportHtml: true }); mds.appendMarkdown(``); - const result = renderMarkdown(mds).element; + const result = store.add(renderMarkdown(mds)).element; assert.strictEqual(result.innerHTML, ``); }); }); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 06ecbdd6396..2d491e240ff 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -11,7 +11,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isSigPipeError, onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { isEqualOrParent } from 'vs/base/common/extpath'; -import { once } from 'vs/base/common/functional'; +import { Event } from 'vs/base/common/event'; import { stripComments } from 'vs/base/common/json'; import { getPathLabel } from 'vs/base/common/labels'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -1310,7 +1310,7 @@ export class CodeApplication extends Disposable { try { const WindowsMutex = await import('@vscode/windows-mutex'); const mutex = new WindowsMutex.Mutex(win32MutexName); - once(this.lifecycleMainService.onWillShutdown)(() => mutex.release()); + Event.once(this.lifecycleMainService.onWillShutdown)(() => mutex.release()); } catch (error) { this.logService.error(error); } diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 7221594d2af..c5466e20b45 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -13,7 +13,7 @@ import { Promises } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ExpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { IPathWithLineAndColumn, isValidBasename, parseLineAndColumnAware, sanitizeFilePath } from 'vs/base/common/extpath'; -import { once } from 'vs/base/common/functional'; +import { Event } from 'vs/base/common/event'; import { getPathLabel } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; import { basename, resolve } from 'vs/base/common/path'; @@ -135,7 +135,7 @@ class CodeMain { bufferLogService.logger = loggerService.createLogger('main', { name: localize('mainLog', "Main") }); // Lifecycle - once(lifecycleMainService.onWillShutdown)(evt => { + Event.once(lifecycleMainService.onWillShutdown)(evt => { fileService.dispose(); configurationService.dispose(); evt.join('instanceLockfile', FSPromises.unlink(environmentMainService.mainLockfile).catch(() => { /* ignored */ })); @@ -279,7 +279,7 @@ class CodeMain { mark('code/willStartMainServer'); mainProcessNodeIpcServer = await nodeIPCServe(environmentMainService.mainIPCHandle); mark('code/didStartMainServer'); - once(lifecycleMainService.onWillShutdown)(() => mainProcessNodeIpcServer.dispose()); + Event.once(lifecycleMainService.onWillShutdown)(() => mainProcessNodeIpcServer.dispose()); } catch (error) { // Handle unexpected errors (the only expected error is EADDRINUSE that diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService.ts b/src/vs/code/electron-sandbox/issue/issueReporterService.ts index e095b3d6d03..ebb776a1013 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterService.ts @@ -672,7 +672,7 @@ export class IssueReporter extends Disposable { } if (issueType !== IssueType.FeatureRequest) { - sourceSelect.append(this.makeOption('', localize('unknown', "Don't know"), false)); + sourceSelect.append(this.makeOption('unknown', localize('unknown', "Don't know"), false)); } if (selected !== -1 && selected < sourceSelect.options.length) { @@ -1056,7 +1056,10 @@ export class IssueReporter extends Disposable { if (extensionsSelector) { const { selectedExtension } = this.issueReporterModel.getData(); reset(extensionsSelector, this.makeOption('', localize('selectExtension', "Select extension"), true), ...extensionOptions.map(extension => makeOption(extension, selectedExtension))); - extensionsSelector.selectedIndex = 0; + + if (!selectedExtension) { + extensionsSelector.selectedIndex = 0; + } this.addEventListener('extension-selector', 'change', (e: Event) => { const selectedExtensionId = (e.target).value; diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index e5c416a9e4b..789b68836b9 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -34,7 +34,7 @@ import { Color } from 'vs/base/common/color'; import { GestureEvent, EventType, Gesture } from 'vs/base/browser/touch'; import { MinimapCharRendererFactory } from 'vs/editor/browser/viewParts/minimap/minimapCharRendererFactory'; import { MinimapPosition, TextModelResolvedOptions } from 'vs/editor/common/model'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; /** * The orthogonal distance to the slider at which dragging "resets". This implements "snapping" @@ -133,7 +133,7 @@ class MinimapOptions { this.minimapLineHeight = minimapLayout.minimapLineHeight; this.minimapCharWidth = Constants.BASE_CHAR_WIDTH * this.fontScale; - this.charRenderer = once(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily)); + this.charRenderer = createSingleCallFunction(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily)); this.defaultBackgroundColor = tokensColorTracker.getColor(ColorId.DefaultBackground); this.backgroundColor = MinimapOptions._getMinimapBackground(theme, this.defaultBackgroundColor); this.foregroundAlpha = MinimapOptions._getMinimapForegroundOpacity(theme); diff --git a/src/vs/editor/browser/viewParts/minimap/minimapPreBaked.ts b/src/vs/editor/browser/viewParts/minimap/minimapPreBaked.ts index 578805adb44..3679e8afad5 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimapPreBaked.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimapPreBaked.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; const charTable: { [hex: string]: number } = { '0': 0, @@ -50,12 +50,12 @@ const encodeData = (data: Uint8ClampedArray, length: string) => { * is use-configurable. */ export const prebakedMiniMaps: { [scale: number]: () => Uint8ClampedArray } = { - 1: once(() => + 1: createSingleCallFunction(() => decodeData( '0000511D6300CF609C709645A78432005642574171487021003C451900274D35D762755E8B629C5BA856AF57BA649530C167D1512A272A3F6038604460398526BCA2A968DB6F8957C768BE5FBE2FB467CF5D8D5B795DC7625B5DFF50DE64C466DB2FC47CD860A65E9A2EB96CB54CE06DA763AB2EA26860524D3763536601005116008177A8705E53AB738E6A982F88BAA35B5F5B626D9C636B449B737E5B7B678598869A662F6B5B8542706C704C80736A607578685B70594A49715A4522E792' ) ), - 2: once(() => + 2: createSingleCallFunction(() => decodeData( '000000000000000055394F383D2800008B8B1F210002000081B1CBCBCC820000847AAF6B9AAF2119BE08B8881AD60000A44FD07DCCF107015338130C00000000385972265F390B406E2437634B4B48031B12B8A0847000001E15B29A402F0000000000004B33460B00007A752C2A0000000000004D3900000084394B82013400ABA5CFC7AD9C0302A45A3E5A98AB000089A43382D97900008BA54AA087A70A0248A6A7AE6DBE0000BF6F94987EA40A01A06DCFA7A7A9030496C32F77891D0000A99FB1A0AFA80603B29AB9CA75930D010C0948354D3900000C0948354F37460D0028BE673D8400000000AF9D7B6E00002B007AA8933400007AA642675C2700007984CFB9C3985B768772A8A6B7B20000CAAECAAFC4B700009F94A6009F840009D09F9BA4CA9C0000CC8FC76DC87F0000C991C472A2000000A894A48CA7B501079BA2C9C69BA20000B19A5D3FA89000005CA6009DA2960901B0A7F0669FB200009D009E00B7890000DAD0F5D092820000D294D4C48BD10000B5A7A4A3B1A50402CAB6CBA6A2000000B5A7A4A3B1A8044FCDADD19D9CB00000B7778F7B8AAE0803C9AB5D3F5D3F00009EA09EA0BAB006039EA0989A8C7900009B9EF4D6B7C00000A9A7816CACA80000ABAC84705D3F000096DA635CDC8C00006F486F266F263D4784006124097B00374F6D2D6D2D6D4A3A95872322000000030000000000008D8939130000000000002E22A5C9CBC70600AB25C0B5C9B400061A2DB04CA67001082AA6BEBEBFC606002321DACBC19E03087AA08B6768380000282FBAC0B8CA7A88AD25BBA5A29900004C396C5894A6000040485A6E356E9442A32CD17EADA70000B4237923628600003E2DE9C1D7B500002F25BBA5A2990000231DB6AFB4A804023025C0B5CAB588062B2CBDBEC0C706882435A75CA20000002326BD6A82A908048B4B9A5A668000002423A09CB4BB060025259C9D8A7900001C1FCAB2C7C700002A2A9387ABA200002626A4A47D6E9D14333163A0C87500004B6F9C2D643A257049364936493647358A34438355497F1A0000A24C1D590000D38DFFBDD4CD3126' ) diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index 1f9525d2024..268e1e6657c 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -7,22 +7,22 @@ import * as nls from 'vs/nls'; export namespace AccessibilityHelpNLS { export const accessibilityHelpTitle = nls.localize('accessibilityHelpTitle', "Accessibility Help"); - export const openingDocs = nls.localize("openingDocs", "Now opening the Accessibility documentation page."); + export const openingDocs = nls.localize("openingDocs", "Opening the Accessibility documentation page."); export const readonlyDiffEditor = nls.localize("readonlyDiffEditor", "You are in a read-only pane of a diff editor."); export const editableDiffEditor = nls.localize("editableDiffEditor", "You are in a pane of a diff editor."); export const readonlyEditor = nls.localize("readonlyEditor", "You are in a read-only code editor."); export const editableEditor = nls.localize("editableEditor", "You are in a code editor."); - export const changeConfigToOnMac = nls.localize("changeConfigToOnMac", "To configure the application to be optimized for usage with a Screen Reader press Command+E now."); - export const changeConfigToOnWinLinux = nls.localize("changeConfigToOnWinLinux", "To configure the application to be optimized for usage with a Screen Reader press Control+E now."); + export const changeConfigToOnMac = nls.localize("changeConfigToOnMac", "Configure the application to be optimized for usage with a Screen Reader (Command+E)."); + export const changeConfigToOnWinLinux = nls.localize("changeConfigToOnWinLinux", "Configure the application to be optimized for usage with a Screen Reader (Control+E)."); export const auto_on = nls.localize("auto_on", "The application is configured to be optimized for usage with a Screen Reader."); export const auto_off = nls.localize("auto_off", "The application is configured to never be optimized for usage with a Screen Reader."); export const screenReaderModeEnabled = nls.localize("screenReaderModeEnabled", "Screen Reader Optimized Mode enabled."); export const screenReaderModeDisabled = nls.localize("screenReaderModeDisabled", "Screen Reader Optimized Mode disabled."); - export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior by pressing {0}."); + export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior {0}."); export const tabFocusModeOnMsgNoKb = nls.localize("tabFocusModeOnMsgNoKb", "Pressing Tab in the current editor will move focus to the next focusable element. The command {0} is currently not triggerable by a keybinding."); export const stickScrollKb = nls.localize("stickScrollKb", "Run the command: Focus Sticky Scroll ({0}) to focus the currently nested scopes."); export const stickScrollNoKb = nls.localize("stickScrollNoKb", "Run the command: Focus Sticky Scroll to focus the currently nested scopes. It is currently not triggerable by a keybinding."); - export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior by pressing {0}."); + export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior {0}."); export const tabFocusModeOffMsgNoKb = nls.localize("tabFocusModeOffMsgNoKb", "Pressing Tab in the current editor will insert the tab character. The command {0} is currently not triggerable by a keybinding."); export const showAccessibilityHelpAction = nls.localize("showAccessibilityHelpAction", "Show Accessibility Help"); } diff --git a/src/vs/editor/contrib/codelens/browser/codeLensCache.ts b/src/vs/editor/contrib/codelens/browser/codeLensCache.ts index 4ab8835ae2f..00249b87bea 100644 --- a/src/vs/editor/contrib/codelens/browser/codeLensCache.ts +++ b/src/vs/editor/contrib/codelens/browser/codeLensCache.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { runWhenIdle } from 'vs/base/common/async'; -import { once } from 'vs/base/common/functional'; +import { Event } from 'vs/base/common/event'; import { LRUCache } from 'vs/base/common/map'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; @@ -60,7 +60,7 @@ export class CodeLensCache implements ICodeLensCache { this._deserialize(raw); // store lens data on shutdown - once(storageService.onWillSaveState)(e => { + Event.once(storageService.onWillSaveState)(e => { if (e.reason === WillSaveStateReason.SHUTDOWN) { storageService.store(key, this._serialize(), StorageScope.WORKSPACE, StorageTarget.MACHINE); } diff --git a/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts index d4082fc5a27..20ba9ff079b 100644 --- a/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { getCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IRange } from 'vs/editor/common/core/range'; @@ -105,7 +105,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu } }; - disposables.add(once(token.onCancellationRequested)(() => context.restoreViewState?.())); + disposables.add(createSingleCallFunction(token.onCancellationRequested)(() => context.restoreViewState?.())); } // Clean up decorations on dispose diff --git a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts index 8c35ee5b269..b94d9d44251 100644 --- a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts +++ b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts @@ -16,7 +16,7 @@ import { EditorScopedLayoutService } from 'vs/editor/standalone/browser/standalo import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { QuickInputController, IQuickInputControllerHost } from 'vs/platform/quickinput/browser/quickInputController'; import { QuickInputService } from 'vs/platform/quickinput/browser/quickInputService'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess'; class EditorScopedQuickInputService extends QuickInputService { @@ -73,7 +73,7 @@ export class StandaloneQuickInputService implements IQuickInputService { const newQuickInputService = quickInputService = this.instantiationService.createInstance(EditorScopedQuickInputService, editor); this.mapEditorToService.set(editor, quickInputService); - once(editor.onDidDispose)(() => { + createSingleCallFunction(editor.onDidDispose)(() => { newQuickInputService.dispose(); this.mapEditorToService.delete(editor); }); diff --git a/src/vs/platform/actions/browser/toolbar.ts b/src/vs/platform/actions/browser/toolbar.ts index e0a3a879b20..d94ded3741e 100644 --- a/src/vs/platform/actions/browser/toolbar.ts +++ b/src/vs/platform/actions/browser/toolbar.ts @@ -8,8 +8,10 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IToolBarOptions, ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IAction, Separator, SubmenuAction, toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { coalesceInPlace } from 'vs/base/common/arrays'; +import { intersection } from 'vs/base/common/collections'; import { BugIndicatingError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; +import { Iterable } from 'vs/base/common/iterator'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -63,9 +65,11 @@ export type IWorkbenchToolBarOptions = IToolBarOptions & { allowContextMenu?: never; /** - * Maximun number of items that can shown. Extra items will be shown in the overflow menu. + * Controls the overflow behavior of the primary group of toolbar. This isthe maximum number of items and id of + * items that should never overflow + * */ - maxNumberOfItems?: number; + overflowBehavior?: { maxItems: number; exempted?: string[] }; }; /** @@ -150,14 +154,22 @@ export class WorkbenchToolBar extends ToolBar { } // count for max - if (this._options?.maxNumberOfItems !== undefined) { + if (this._options?.overflowBehavior !== undefined) { + + const exemptedIds = intersection(new Set(this._options.overflowBehavior.exempted), Iterable.map(primary, a => a.id)); + const maxItems = this._options.overflowBehavior.maxItems - exemptedIds.size; + let count = 0; for (let i = 0; i < primary.length; i++) { const action = primary[i]; if (!action) { continue; } - if (++count >= this._options.maxNumberOfItems) { + count++; + if (exemptedIds.has(action.id)) { + continue; + } + if (count >= maxItems) { primary[i] = undefined!; extraSecondary[i] = action; } diff --git a/src/vs/platform/extensionManagement/common/extensionTipsService.ts b/src/vs/platform/extensionManagement/common/extensionTipsService.ts index 9c99ae68bb0..7bdc72134e5 100644 --- a/src/vs/platform/extensionManagement/common/extensionTipsService.ts +++ b/src/vs/platform/extensionManagement/common/extensionTipsService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IConfigBasedExtensionTip as IRawConfigBasedExtensionTip } from 'vs/base/common/product'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; @@ -154,11 +154,11 @@ export abstract class AbstractNativeExtensionTipsService extends ExtensionTipsSe 3s has come out to be the good number to fetch and prompt important exe based recommendations Also fetch important exe based recommendations for reporting telemetry */ - this._register(disposableTimeout(async () => { + disposableTimeout(async () => { await this.collectTips(); this.promptHighImportanceExeBasedTip(); this.promptMediumImportanceExeBasedTip(); - }, 3000)); + }, 3000, this._store); } override async getImportantExecutableBasedTips(): Promise { @@ -243,7 +243,8 @@ export abstract class AbstractNativeExtensionTipsService extends ExtensionTipsSe } case RecommendationsNotificationResult.TooMany: { // Too many notifications. Schedule the prompt after one hour - const disposable = this._register(disposableTimeout(() => { disposable.dispose(); this.promptHighImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */)); + const disposable = this._register(new MutableDisposable()); + disposable.value = disposableTimeout(() => { disposable.dispose(); this.promptHighImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */); break; } } @@ -263,7 +264,8 @@ export abstract class AbstractNativeExtensionTipsService extends ExtensionTipsSe const promptInterval = 7 * 24 * 60 * 60 * 1000; // 7 Days if (timeSinceLastPrompt < promptInterval) { // Wait until interval and prompt - const disposable = this._register(disposableTimeout(() => { disposable.dispose(); this.promptMediumImportanceExeBasedTip(); }, promptInterval - timeSinceLastPrompt)); + const disposable = this._register(new MutableDisposable()); + disposable.value = disposableTimeout(() => { disposable.dispose(); this.promptMediumImportanceExeBasedTip(); }, promptInterval - timeSinceLastPrompt); return; } @@ -278,7 +280,8 @@ export abstract class AbstractNativeExtensionTipsService extends ExtensionTipsSe this.addToRecommendedExecutables(tips[0].exeName, tips); // Schedule the next recommendation for next internval - const disposable1 = this._register(disposableTimeout(() => { disposable1.dispose(); this.promptMediumImportanceExeBasedTip(); }, promptInterval)); + const disposable1 = this._register(new MutableDisposable()); + disposable1.value = disposableTimeout(() => { disposable1.dispose(); this.promptMediumImportanceExeBasedTip(); }, promptInterval); break; } case RecommendationsNotificationResult.Ignored: @@ -295,7 +298,8 @@ export abstract class AbstractNativeExtensionTipsService extends ExtensionTipsSe } case RecommendationsNotificationResult.TooMany: { // Too many notifications. Schedule the prompt after one hour - const disposable2 = this._register(disposableTimeout(() => { disposable2.dispose(); this.promptMediumImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */)); + const disposable2 = this._register(new MutableDisposable()); + disposable2.value = disposableTimeout(() => { disposable2.dispose(); this.promptMediumImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */); break; } } diff --git a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts index 2ff14dbaa44..1e543bab44d 100644 --- a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts @@ -8,7 +8,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isCancellationError } from 'vs/base/common/errors'; import { matchesContiguousSubString, matchesPrefix, matchesWords, or } from 'vs/base/common/filters'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { LRUCache } from 'vs/base/common/map'; import { TfIdfCalculator, normalizeTfIdfScores } from 'vs/base/common/tfIdf'; @@ -71,7 +71,7 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc return []; } - const runTfidf = once(() => { + const runTfidf = createSingleCallFunction(() => { const tfidf = new TfIdfCalculator(); tfidf.updateDocuments(allCommandPicks.map(commandPick => ({ key: commandPick.commandId, diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts index 0f2fa5c8f3c..cb35e451aa1 100644 --- a/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -5,7 +5,7 @@ import { DeferredPromise } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { once } from 'vs/base/common/functional'; +import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DefaultQuickAccessFilterValue, Extensions, IQuickAccessController, IQuickAccessOptions, IQuickAccessProvider, IQuickAccessProviderDescriptor, IQuickAccessProviderRunOptions, IQuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; @@ -112,7 +112,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon let pickPromise: DeferredPromise | undefined = undefined; if (pick) { pickPromise = new DeferredPromise(); - disposables.add(once(picker.onWillAccept)(e => { + disposables.add(Event.once(picker.onWillAccept)(e => { e.veto(); picker.hide(); })); @@ -131,7 +131,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon // Finally, trigger disposal and cancellation when the picker // hides depending on items selected or not. - once(picker.onDidHide)(() => { + Event.once(picker.onDidHide)(() => { if (picker.selectedItems.length === 0) { cts.cancel(); } diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts index 4d5c2eb209a..569327c095d 100644 --- a/src/vs/platform/storage/electron-main/storageMainService.ts +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { once } from 'vs/base/common/functional'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IStorage } from 'vs/base/parts/storage/common/storage'; @@ -171,7 +170,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic const applicationStorage = new ApplicationStorageMain(this.getStorageOptions(), this.userDataProfilesService, this.logService, this.fileService); - this._register(once(applicationStorage.onDidCloseStorage)(() => { + this._register(Event.once(applicationStorage.onDidCloseStorage)(() => { this.logService.trace(`StorageMainService: closed application storage`); })); @@ -202,7 +201,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic profile }))); - this._register(once(profileStorage.onDidCloseStorage)(() => { + this._register(Event.once(profileStorage.onDidCloseStorage)(() => { this.logService.trace(`StorageMainService: closed profile storage (${profile.name})`); this.mapProfileToStorage.delete(profile.id); @@ -241,7 +240,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic workspaceStorage = this._register(this.createWorkspaceStorage(workspace)); this.mapWorkspaceToStorage.set(workspace.id, workspaceStorage); - this._register(once(workspaceStorage.onDidCloseStorage)(() => { + this._register(Event.once(workspaceStorage.onDidCloseStorage)(() => { this.logService.trace(`StorageMainService: closed workspace storage (${workspace.id})`); this.mapWorkspaceToStorage.delete(workspace.id); diff --git a/src/vs/platform/url/electron-main/electronUrlListener.ts b/src/vs/platform/url/electron-main/electronUrlListener.ts index da07ede149f..ffe2f5bb188 100644 --- a/src/vs/platform/url/electron-main/electronUrlListener.ts +++ b/src/vs/platform/url/electron-main/electronUrlListener.ts @@ -85,7 +85,7 @@ export class ElectronURLListener extends Disposable { } else { logService.trace('ElectronURLListener: waiting for window to be ready to handle URLs...'); - this._register(Event.once(windowsMainService.onDidSignalReadyWindow)(this.flush)); + this._register(Event.once(windowsMainService.onDidSignalReadyWindow)(() => this.flush())); } } diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index c8cd1d8ac21..b0776afe8dc 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -424,7 +424,10 @@ class AutoSync extends Disposable { } private waitUntilNextIntervalAndSync(): void { - this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING, false), this.interval); + this.intervalHandler.value = disposableTimeout(() => { + this.sync(AutoSync.INTERVAL_SYNCING, false); + this.intervalHandler.value = undefined; + }, this.interval); } sync(reason: string, disableCache: boolean): Promise { diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 67d7113b000..776d0650423 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -12,7 +12,6 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { CharCode } from 'vs/base/common/charCode'; import { Emitter, Event } from 'vs/base/common/event'; import { isWindowsDriveLetter, parseLineAndColumnAware, sanitizeFilePath, toSlashes } from 'vs/base/common/extpath'; -import { once } from 'vs/base/common/functional'; import { getPathLabel } from 'vs/base/common/labels'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; @@ -1464,9 +1463,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic this._onDidChangeWindowsCount.fire({ oldCount: this.getWindowCount() - 1, newCount: this.getWindowCount() }); // Window Events - once(createdWindow.onDidSignalReady)(() => this._onDidSignalReadyWindow.fire(createdWindow)); - once(createdWindow.onDidClose)(() => this.onWindowClosed(createdWindow)); - once(createdWindow.onDidDestroy)(() => this._onDidDestroyWindow.fire(createdWindow)); + Event.once(createdWindow.onDidSignalReady)(() => this._onDidSignalReadyWindow.fire(createdWindow)); + Event.once(createdWindow.onDidClose)(() => this.onWindowClosed(createdWindow)); + Event.once(createdWindow.onDidDestroy)(() => this._onDidDestroyWindow.fire(createdWindow)); createdWindow.onDidTriggerSystemContextMenu(({ x, y }) => this._onDidTriggerSystemContextMenu.fire({ window: createdWindow, x, y })); const webContents = assertIsDefined(createdWindow.win?.webContents); diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 3c29e080e8c..8c9da3c5c8e 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -10,7 +10,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { hash } from 'vs/base/common/hash'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; @@ -979,7 +979,7 @@ class TestObservers { return { onDidChangeTest: current.tests.onDidChangeTests, get tests() { return [...current.tests.rootTests].map(t => t.revived); }, - dispose: once(() => { + dispose: createSingleCallFunction(() => { if (--current.observers === 0) { this.proxy.$unsubscribeFromDiffs(); this.current = undefined; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 9055980c1e4..48159f1a07e 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -6,7 +6,7 @@ import { asArray, coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { VSBuffer, encodeBase64 } from 'vs/base/common/buffer'; import { IDataTransferFile, IDataTransferItem, UriList } from 'vs/base/common/dataTransfer'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import * as htmlContent from 'vs/base/common/htmlContent'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ResourceMap, ResourceSet } from 'vs/base/common/map'; @@ -2076,7 +2076,7 @@ export namespace DataTransferItem { const file = item.fileData; if (file) { return new types.InternalFileDataTransferItem( - new types.DataTransferFile(file.name, URI.revive(file.uri), file.id, once(() => resolveFileData(file.id)))); + new types.DataTransferFile(file.name, URI.revive(file.uri), file.id, createSingleCallFunction(() => resolveFileData(file.id)))); } if (mime === Mimes.uriList && item.uriListData) { diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index e5b880451cb..5a9796b18db 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -659,6 +659,8 @@ export class ResourceListDnDHandler implements IListDragAndDrop { } drop(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void { } + + dispose(): void { } } //#endregion diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 3fc1bdc4e82..72be1131bda 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -351,7 +351,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { if (account.canSignOut) { const signOutAction = disposables.add(new Action('signOut', localize('signOut', "Sign Out"), undefined, true, async () => { const allSessions = await this.authenticationService.getSessions(providerId); - const sessionsForAccount = allSessions.filter(s => s.account.id === account.id); + const sessionsForAccount = allSessions.filter(s => s.account.label === account.label); return await this.authenticationService.removeAccountSessions(providerId, account.label, sessionsForAccount); })); providerSubMenuActions.push(signOutAction); @@ -399,34 +399,35 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { private async addOrUpdateAccount(providerId: string, account: AuthenticationSessionAccount): Promise { let accounts = this.groupedAccounts.get(providerId); - if (accounts) { - const existingAccount = accounts.find(a => a.id === account.id); - if (existingAccount) { - // Update the label if it has changed - if (existingAccount.label !== account.label) { - existingAccount.label = account.label; - } - return; - } - } else { + if (!accounts) { accounts = []; this.groupedAccounts.set(providerId, accounts); } const sessionFromEmbedder = await this.sessionFromEmbedder.value; - // If the session stored from the embedder allows sign out, then we can treat it and all others as sign out-able - let canSignOut = !!sessionFromEmbedder?.canSignOut; - if (!canSignOut) { - if (sessionFromEmbedder?.id) { - const sessions = (await this.authenticationService.getSessions(providerId)).filter(s => s.account.id === account.id); - canSignOut = !sessions.some(s => s.id === sessionFromEmbedder.id); - } else { - // The default if we don't have a session from the embedder is to allow sign out - canSignOut = true; - } + let canSignOut = true; + if ( + sessionFromEmbedder // if we have a session from the embedder + && !sessionFromEmbedder.canSignOut // and that session says we can't sign out + && (await this.authenticationService.getSessions(providerId)) // and that session is associated with the account we are adding/updating + .some(s => + s.id === sessionFromEmbedder.id + && s.account.id === account.id + ) + ) { + canSignOut = false; } - accounts.push({ ...account, canSignOut }); + const existingAccount = accounts.find(a => a.label === account.label); + if (existingAccount) { + // if we have an existing account and we discover that we + // can't sign out of it, update the account to mark it as "can't sign out" + if (!canSignOut) { + existingAccount.canSignOut = canSignOut; + } + } else { + accounts.push({ ...account, canSignOut }); + } } private removeAccount(providerId: string, account: AuthenticationSessionAccount): void { diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 42870c2ccdc..2e247ec5e88 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -47,7 +47,7 @@ import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_EDITOR_GROUP_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_PINNED_EDITOR_COMMAND_ID, CLOSE_SAVED_EDITORS_COMMAND_ID, GOTO_NEXT_CHANGE, GOTO_PREVIOUS_CHANGE, KEEP_EDITOR_COMMAND_ID, PIN_EDITOR_COMMAND_ID, SHOW_EDITORS_IN_GROUP, SPLIT_EDITOR_DOWN, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE, TOGGLE_DIFF_SIDE_BY_SIDE, TOGGLE_KEEP_EDITORS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, setup as registerEditorCommands, REOPEN_WITH_COMMAND_ID, - TOGGLE_LOCK_GROUP_COMMAND_ID, UNLOCK_GROUP_COMMAND_ID, SPLIT_EDITOR_IN_GROUP, JOIN_EDITOR_IN_GROUP, FOCUS_FIRST_SIDE_EDITOR, FOCUS_SECOND_SIDE_EDITOR, TOGGLE_SPLIT_EDITOR_IN_GROUP_LAYOUT + TOGGLE_LOCK_GROUP_COMMAND_ID, UNLOCK_GROUP_COMMAND_ID, SPLIT_EDITOR_IN_GROUP, JOIN_EDITOR_IN_GROUP, FOCUS_FIRST_SIDE_EDITOR, FOCUS_SECOND_SIDE_EDITOR, TOGGLE_SPLIT_EDITOR_IN_GROUP_LAYOUT, SPLIT_EDITOR } from 'vs/workbench/browser/parts/editor/editorCommands'; import { inQuickPickContext, getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -411,7 +411,7 @@ const CLOSE_ORDER = 1000000; // towards the far end // Editor Title Menu: Split Editor appendEditorToolItem( { - id: SplitEditorAction.ID, + id: SPLIT_EDITOR, title: localize('splitEditorRight', "Split Editor Right"), icon: Codicon.splitHorizontal }, @@ -426,7 +426,7 @@ appendEditorToolItem( appendEditorToolItem( { - id: SplitEditorAction.ID, + id: SPLIT_EDITOR, title: localize('splitEditorDown', "Split Editor Down"), icon: Codicon.splitVertical }, diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index bbd93707d10..e2b3f260dec 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent, SideBySideEditor, EditorCloseContext } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent, SideBySideEditor, EditorCloseContext, IEditorPane } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IEditorGroup, GroupDirection, IMergeGroupOptions, GroupsOrder, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -82,7 +82,7 @@ export function getEditorPartOptions(configurationService: IConfigurationService return options; } -export interface IEditorGroupsAccessor { +export interface IEditorGroupsView { readonly groups: IEditorGroupView[]; readonly activeGroup: IEditorGroupView; @@ -148,6 +148,8 @@ export interface IEditorGroupView extends IDisposable, ISerializableView, IEdito notifyIndexChanged(newIndex: number): void; + openEditor(editor: EditorInput, options?: IEditorOptions, internalOptions?: IInternalEditorOpenOptions): Promise; + relayout(): void; } @@ -200,6 +202,11 @@ export interface IInternalEditorOpenOptions extends IInternalEditorTitleControlO * opened in one of the sides. */ supportSideBySide?: SideBySideEditor.ANY | SideBySideEditor.BOTH; + + /** + * When set to `true`, pass DOM focus into the tab control. + */ + focusTabControl?: boolean; } export interface IInternalEditorCloseOptions extends IInternalEditorTitleControlOptions { diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index bcb01677992..87f58869e95 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -13,7 +13,7 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { GoFilter, IHistoryService } from 'vs/workbench/services/history/common/history'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR } from 'vs/workbench/browser/parts/editor/editorCommands'; import { IEditorGroupsService, IEditorGroup, GroupsArrangement, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -68,7 +68,7 @@ abstract class AbstractSplitEditorAction extends Action2 { export class SplitEditorAction extends AbstractSplitEditorAction { - static readonly ID = 'workbench.action.splitEditor'; + static readonly ID = SPLIT_EDITOR; constructor() { super({ diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 77c8985611a..0a7a26c37c1 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -73,6 +73,7 @@ export const DIFF_FOCUS_OTHER_SIDE = 'workbench.action.compareEditor.focusOtherS export const DIFF_OPEN_SIDE = 'workbench.action.compareEditor.openSide'; export const TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE = 'toggle.diff.ignoreTrimWhitespace'; +export const SPLIT_EDITOR = 'workbench.action.splitEditor'; export const SPLIT_EDITOR_UP = 'workbench.action.splitEditorUp'; export const SPLIT_EDITOR_DOWN = 'workbench.action.splitEditorDown'; export const SPLIT_EDITOR_LEFT = 'workbench.action.splitEditorLeft'; @@ -98,6 +99,13 @@ export const API_OPEN_EDITOR_COMMAND_ID = '_workbench.open'; export const API_OPEN_DIFF_EDITOR_COMMAND_ID = '_workbench.diff'; export const API_OPEN_WITH_EDITOR_COMMAND_ID = '_workbench.openWith'; +export const EDITOR_CORE_NAVIGATION_COMMANDS = [ + SPLIT_EDITOR, + CLOSE_EDITOR_COMMAND_ID, + UNPIN_EDITOR_COMMAND_ID, + UNLOCK_GROUP_COMMAND_ID +]; + export interface ActiveEditorMoveCopyArguments { to: 'first' | 'last' | 'left' | 'right' | 'up' | 'down' | 'center' | 'position' | 'previous' | 'next'; by: 'tab' | 'group'; diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 73da73d5f3a..be6231c1a95 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -20,7 +20,7 @@ import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { CodeDataTransfers, containsDragType, Extensions as DragAndDropExtensions, IDragAndDropContributionRegistry, LocalSelectionTransfer } from 'vs/platform/dnd/browser/dnd'; import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, extractTreeDropData, ResourcesDropHandler } from 'vs/workbench/browser/dnd'; -import { fillActiveEditorViewState, IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { fillActiveEditorViewState, IEditorGroupsView, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { EditorInputCapabilities, IEditorIdentifier, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EDITOR_DRAG_AND_DROP_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BORDER, EDITOR_DROP_INTO_PROMPT_FOREGROUND } from 'vs/workbench/common/theme'; import { GroupDirection, IEditorGroupsService, IMergeGroupOptions, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -62,7 +62,7 @@ class DropOverlay extends Themable { private readonly enableDropIntoEditor: boolean; constructor( - private accessor: IEditorGroupsAccessor, + private groupsView: IEditorGroupsView, private groupView: IEditorGroupView, @IThemeService themeService: IThemeService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -237,7 +237,7 @@ class DropOverlay extends Themable { if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); if (Array.isArray(data)) { - return this.accessor.getGroup(data[0].identifier); + return this.groupsView.getGroup(data[0].identifier); } } @@ -245,7 +245,7 @@ class DropOverlay extends Themable { else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); if (Array.isArray(data)) { - return this.accessor.getGroup(data[0].identifier.groupId); + return this.groupsView.getGroup(data[0].identifier.groupId); } } @@ -258,7 +258,7 @@ class DropOverlay extends Themable { const ensureTargetGroup = () => { let targetGroup: IEditorGroupView; if (typeof splitDirection === 'number') { - targetGroup = this.accessor.addGroup(this.groupView, splitDirection); + targetGroup = this.groupsView.addGroup(this.groupView, splitDirection); } else { targetGroup = this.groupView; } @@ -270,7 +270,7 @@ class DropOverlay extends Themable { if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); if (Array.isArray(data)) { - const sourceGroup = this.accessor.getGroup(data[0].identifier); + const sourceGroup = this.groupsView.getGroup(data[0].identifier); if (sourceGroup) { if (typeof splitDirection !== 'number' && sourceGroup === this.groupView) { return; @@ -280,9 +280,9 @@ class DropOverlay extends Themable { let targetGroup: IEditorGroupView | undefined; if (typeof splitDirection === 'number') { if (this.isCopyOperation(event)) { - targetGroup = this.accessor.copyGroup(sourceGroup, this.groupView, splitDirection); + targetGroup = this.groupsView.copyGroup(sourceGroup, this.groupView, splitDirection); } else { - targetGroup = this.accessor.moveGroup(sourceGroup, this.groupView, splitDirection); + targetGroup = this.groupsView.moveGroup(sourceGroup, this.groupView, splitDirection); } } @@ -293,11 +293,11 @@ class DropOverlay extends Themable { mergeGroupOptions = { mode: MergeGroupMode.COPY_EDITORS }; } - this.accessor.mergeGroup(sourceGroup, this.groupView, mergeGroupOptions); + this.groupsView.mergeGroup(sourceGroup, this.groupView, mergeGroupOptions); } if (targetGroup) { - this.accessor.activateGroup(targetGroup); + this.groupsView.activateGroup(targetGroup); } } @@ -311,7 +311,7 @@ class DropOverlay extends Themable { if (Array.isArray(data)) { const draggedEditor = data[0].identifier; - const sourceGroup = this.accessor.getGroup(draggedEditor.groupId); + const sourceGroup = this.groupsView.getGroup(draggedEditor.groupId); if (sourceGroup) { const copyEditor = this.isCopyOperation(event, draggedEditor); let targetGroup: IEditorGroupView | undefined = undefined; @@ -320,7 +320,7 @@ class DropOverlay extends Themable { // and we are configured to close empty editor groups, we can // rather move the entire editor group according to the direction if (this.editorGroupService.partOptions.closeEmptyGroups && sourceGroup.count === 1 && typeof splitDirection === 'number' && !copyEditor) { - targetGroup = this.accessor.moveGroup(sourceGroup, this.groupView, splitDirection); + targetGroup = this.groupsView.moveGroup(sourceGroup, this.groupView, splitDirection); } // In any other case do a normal move/copy operation @@ -391,7 +391,7 @@ class DropOverlay extends Themable { } private positionOverlay(mousePosX: number, mousePosY: number, isDraggingGroup: boolean, enableSplitting: boolean): void { - const preferSplitVertically = this.accessor.partOptions.openSideBySideDirection === 'right'; + const preferSplitVertically = this.groupsView.partOptions.openSideBySideDirection === 'right'; const editorControlWidth = this.groupView.element.clientWidth; const editorControlHeight = this.groupView.element.clientHeight - this.getOverlayOffsetHeight(); @@ -531,7 +531,7 @@ class DropOverlay extends Themable { private getOverlayOffsetHeight(): number { // With tabs and opened editors: use the area below tabs as drop target - if (!this.groupView.isEmpty && this.accessor.partOptions.showTabs) { + if (!this.groupView.isEmpty && this.groupsView.partOptions.showTabs) { return this.groupView.titleHeight.offset; } @@ -587,7 +587,7 @@ export class EditorDropTarget extends Themable { private readonly groupTransfer = LocalSelectionTransfer.getInstance(); constructor( - private accessor: IEditorGroupsAccessor, + private groupsView: IEditorGroupsView, private container: HTMLElement, private readonly delegate: IEditorDropTargetDelegate, @IThemeService themeService: IThemeService, @@ -649,7 +649,7 @@ export class EditorDropTarget extends Themable { if (!this.overlay) { const targetGroupView = this.findTargetGroupView(target); if (targetGroupView) { - this._overlay = this.instantiationService.createInstance(DropOverlay, this.accessor, targetGroupView); + this._overlay = this.instantiationService.createInstance(DropOverlay, this.groupsView, targetGroupView); } } } @@ -671,7 +671,7 @@ export class EditorDropTarget extends Themable { } private findTargetGroupView(child: HTMLElement): IEditorGroupView | undefined { - const groups = this.accessor.groups; + const groups = this.groupsView.groups; return groups.find(groupView => isAncestor(child, groupView.element) || this.delegate.containsGroup?.(groupView)); } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 0d116c056b1..f126367be2b 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -28,7 +28,7 @@ import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DeferredPromise, Promises, RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; -import { IEditorGroupsAccessor, IEditorGroupView, fillActiveEditorViewState, EditorServiceImpl, IEditorGroupTitleHeight, IInternalEditorOpenOptions, IInternalMoveCopyOptions, IInternalEditorCloseOptions, IInternalEditorTitleControlOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, IEditorGroupView, fillActiveEditorViewState, EditorServiceImpl, IEditorGroupTitleHeight, IInternalEditorOpenOptions, IInternalMoveCopyOptions, IInternalEditorCloseOptions, IInternalEditorTitleControlOptions } from 'vs/workbench/browser/parts/editor/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IAction } from 'vs/base/common/actions'; @@ -58,16 +58,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region factory - static createNew(accessor: IEditorGroupsAccessor, index: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, accessor, null, index); + static createNew(groupsView: IEditorGroupsView, index: number, instantiationService: IInstantiationService): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, groupsView, null, index); } - static createFromSerialized(serialized: ISerializedEditorGroupModel, accessor: IEditorGroupsAccessor, index: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, accessor, serialized, index); + static createFromSerialized(serialized: ISerializedEditorGroupModel, groupsView: IEditorGroupsView, index: number, instantiationService: IInstantiationService): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, groupsView, serialized, index); } - static createCopy(copyFrom: IEditorGroupView, accessor: IEditorGroupsAccessor, index: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, accessor, copyFrom, index); + static createCopy(copyFrom: IEditorGroupView, groupsView: IEditorGroupsView, index: number, instantiationService: IInstantiationService): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, groupsView, copyFrom, index); } //#endregion @@ -133,7 +133,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { readonly whenRestored = this.whenRestoredPromise.p; constructor( - private accessor: IEditorGroupsAccessor, + private groupsView: IEditorGroupsView, from: IEditorGroupView | ISerializedEditorGroupModel | null, private _index: number, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -198,7 +198,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.element.appendChild(this.titleContainer); // Title control - this.titleControl = this._register(this.scopedInstantiationService.createInstance(EditorTitleControl, this.titleContainer, this.accessor, this, this.model)); + this.titleControl = this._register(this.scopedInstantiationService.createInstance(EditorTitleControl, this.titleContainer, this.groupsView, this, this.model)); // Editor container this.editorContainer = document.createElement('div'); @@ -318,7 +318,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (this.isEmpty && e.button === 1 /* Middle Button */) { EventHelper.stop(e, true); - this.accessor.removeGroup(this); + this.groupsView.removeGroup(this); } })); } @@ -452,8 +452,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } private updateTitleContainer(): void { - this.titleContainer.classList.toggle('tabs', this.accessor.partOptions.showTabs); - this.titleContainer.classList.toggle('show-file-icons', this.accessor.partOptions.showIcons); + this.titleContainer.classList.toggle('tabs', this.groupsView.partOptions.showTabs); + this.titleContainer.classList.toggle('show-file-icons', this.groupsView.partOptions.showIcons); } private restoreEditors(from: IEditorGroupView | ISerializedEditorGroupModel | null): Promise | undefined { @@ -489,7 +489,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // stolen accidentally on startup when the user already // clicked somewhere. - if (this.accessor.activeGroup === this && activeElement === document.activeElement) { + if (this.groupsView.activeGroup === this && activeElement === document.activeElement) { this.focus(); } }); @@ -503,10 +503,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._register(this.model.onDidModelChange(e => this.onDidGroupModelChange(e))); // Option Changes - this._register(this.accessor.onDidChangeEditorPartOptions(e => this.onDidChangeEditorPartOptions(e))); + this._register(this.groupsView.onDidChangeEditorPartOptions(e => this.onDidChangeEditorPartOptions(e))); // Visibility - this._register(this.accessor.onDidVisibilityChange(e => this.onDidVisibilityChange(e))); + this._register(this.groupsView.onDidVisibilityChange(e => this.onDidVisibilityChange(e))); } private onDidGroupModelChange(e: IGroupModelChangeEvent): void { @@ -600,7 +600,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } private canDispose(editor: EditorInput): boolean { - for (const groupView of this.accessor.groups) { + for (const groupView of this.groupsView.groups) { if (groupView instanceof EditorGroupView && groupView.model.contains(editor, { strictEquals: true, // only if this input is not shared across editor groups supportSideBySide: SideBySideEditor.ANY // include any side of an opened side by side editor @@ -946,8 +946,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region openEditor() - async openEditor(editor: EditorInput, options?: IEditorOptions): Promise { + async openEditor(editor: EditorInput, options?: IEditorOptions, internalOptions?: IInternalEditorOpenOptions): Promise { return this.doOpenEditor(editor, options, { + // Appply given internal open options + ...internalOptions, // Allow to match on a side-by-side editor when same // editor is opened on both sides. In that case we // do not want to open a new editor but reuse that one. @@ -969,7 +971,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Determine options const pinned = options?.sticky - || !this.accessor.partOptions.enablePreview + || !this.groupsView.partOptions.enablePreview || editor.isDirty() || (options?.pinned ?? typeof options?.index === 'number' /* unless specified, prefer to pin when opening with index */) || (typeof options?.index === 'number' && this.model.isSticky(options.index)) @@ -1030,10 +1032,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if ( isNew && // only if this editor was new for the group this.count === 1 && // only when this editor was the first editor in the group - this.accessor.groups.length > 1 // only when there are more than one groups open + this.groupsView.groups.length > 1 // only when there are more than one groups open ) { // only when the editor identifier is configured as such - if (openedEditor.editorId && this.accessor.partOptions.autoLockGroups?.has(openedEditor.editorId)) { + if (openedEditor.editorId && this.groupsView.partOptions.autoLockGroups?.has(openedEditor.editorId)) { this.lock(true); } } @@ -1043,9 +1045,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Finally make sure the group is active or restored as instructed if (activateGroup) { - this.accessor.activateGroup(this); + this.groupsView.activateGroup(this); } else if (restoreGroup) { - this.accessor.restoreGroup(this); + this.groupsView.restoreGroup(this); } return showEditorResult; @@ -1090,7 +1092,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Show in title control after editor control because some actions depend on it // but respect the internal options in case title control updates should skip. if (!internalOptions?.skipTitleUpdate) { - this.titleControl.openEditor(editor); + this.titleControl.openEditor(editor, internalOptions); } return openEditorPromise; @@ -1322,7 +1324,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return true; } - private doCloseEditor(editor: EditorInput, focusNext = (this.accessor.activeGroup === this), internalOptions?: IInternalEditorCloseOptions): void { + private doCloseEditor(editor: EditorInput, focusNext = (this.groupsView.activeGroup === this), internalOptions?: IInternalEditorCloseOptions): void { // Forward to title control unless skipped via internal options if (!internalOptions?.skipTitleUpdate) { @@ -1345,7 +1347,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } - private doCloseActiveEditor(focusNext = (this.accessor.activeGroup === this), internalOptions?: IInternalEditorCloseOptions): void { + private doCloseActiveEditor(focusNext = (this.groupsView.activeGroup === this), internalOptions?: IInternalEditorCloseOptions): void { const editorToClose = this.activeEditor; const restoreFocus = this.shouldRestoreFocus(this.element); @@ -1356,15 +1358,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // optimization, this group (if active) would first trigger a active editor change // event because it became empty, only to then trigger another one when the next // group gets active. - const closeEmptyGroup = this.accessor.partOptions.closeEmptyGroups; + const closeEmptyGroup = this.groupsView.partOptions.closeEmptyGroups; if (closeEmptyGroup && this.active && this.count === 1) { - const mostRecentlyActiveGroups = this.accessor.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); + const mostRecentlyActiveGroups = this.groupsView.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); const nextActiveGroup = mostRecentlyActiveGroups[1]; // [0] will be the current one, so take [1] if (nextActiveGroup) { if (restoreFocus) { nextActiveGroup.focus(); } else { - this.accessor.activateGroup(nextActiveGroup); + this.groupsView.activateGroup(nextActiveGroup); } } } @@ -1380,7 +1382,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const preserveFocus = !focusNext; let activation: EditorActivation | undefined = undefined; - if (preserveFocus && this.accessor.activeGroup !== this) { + if (preserveFocus && this.groupsView.activeGroup !== this) { // If we are opening the next editor in an inactive group // without focussing it, ensure we preserve the editor // group sizes in case that group is minimized. @@ -1420,7 +1422,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Remove empty group if we should if (closeEmptyGroup) { - this.accessor.removeGroup(this); + this.groupsView.removeGroup(this); } } } @@ -1489,7 +1491,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // The only exception is when the same editor is opened on both sides of a side // by side editor (https://github.com/microsoft/vscode/issues/138442) - if (this.accessor.groups.some(groupView => { + if (this.groupsView.groups.some(groupView => { if (groupView === this) { return false; // skip (we already handled our group above) } @@ -1705,8 +1707,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // If the group is empty and the request is to close all editors, we still close // the editor group is the related setting to close empty groups is enabled for // a convenient way of removing empty editor groups for the user. - if (this.accessor.partOptions.closeEmptyGroups) { - this.accessor.removeGroup(this); + if (this.groupsView.partOptions.closeEmptyGroups) { + this.groupsView.removeGroup(this); } return true; @@ -1826,7 +1828,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region Locking get isLocked(): boolean { - if (this.accessor.groups.length === 1) { + if (this.groupsView.groups.length === 1) { // Special case: if only 1 group is opened, never report it as locked // to ensure editors can always open in the "default" editor group return false; @@ -1836,7 +1838,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } lock(locked: boolean): void { - if (this.accessor.groups.length === 1) { + if (this.groupsView.groups.length === 1) { // Special case: if only 1 group is opened, never allow to lock // to ensure editors can always open in the "default" editor group locked = false; @@ -1869,7 +1871,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleContainer.style.removeProperty('--title-border-bottom-color'); } - const { showTabs } = this.accessor.partOptions; + const { showTabs } = this.groupsView.partOptions; this.titleContainer.style.backgroundColor = this.getColor(showTabs ? EDITOR_GROUP_HEADER_TABS_BACKGROUND : EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND) || ''; // Editor container diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index af10ae9c109..83287009c56 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -14,7 +14,7 @@ import { IView, orthogonal, LayoutPriority, IViewSize, Direction, SerializableGr import { GroupIdentifier, EditorInputWithOptions, IEditorPartOptions, IEditorPartOptionsChangeEvent, GroupModelChangeKind } from 'vs/workbench/common/editor'; import { EDITOR_GROUP_BORDER, EDITOR_PANE_BACKGROUND } from 'vs/workbench/common/theme'; import { distinct, coalesce, firstOrDefault } from 'vs/base/common/arrays'; -import { IEditorGroupsAccessor, IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions } from 'vs/workbench/browser/parts/editor/editor'; import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -80,7 +80,7 @@ class GridWidgetView implements IView { } } -export class EditorPart extends Part implements IEditorGroupsService, IEditorGroupsAccessor, IEditorDropService { +export class EditorPart extends Part implements IEditorGroupsService, IEditorGroupsView, IEditorDropService { declare readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index 353dc2cc422..253820fd98a 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -24,7 +24,7 @@ import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillEditorsDragData } from 'vs/workbench/browser/dnd'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, IEditorGroupView, IInternalEditorOpenOptions } from 'vs/workbench/browser/parts/editor/editor'; import { IEditorCommandsContext, EditorResourceAccessor, IEditorPartOptions, SideBySideEditor, EditorsOrder, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ResourceContextKey, ActiveEditorPinnedContext, ActiveEditorStickyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ActiveEditorAvailableEditorIdsContext, applyAvailableEditorIds } from 'vs/workbench/common/contextkeys'; @@ -39,6 +39,7 @@ import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsD import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IEditorTitleControlDimensions } from 'vs/workbench/browser/parts/editor/editorTitleControl'; import { IReadonlyEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; +import { EDITOR_CORE_NAVIGATION_COMMANDS } from 'vs/workbench/browser/parts/editor/editorCommands'; export interface IToolbarActions { readonly primary: IAction[]; @@ -73,7 +74,7 @@ export class EditorCommandsContextActionRunner extends ActionRunner { export interface IEditorTabsControl extends IDisposable { updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void; - openEditor(editor: EditorInput): boolean; + openEditor(editor: EditorInput, options?: IInternalEditorOpenOptions): boolean; openEditors(editors: EditorInput[]): boolean; beforeCloseEditor(editor: EditorInput): void; closeEditor(editor: EditorInput): void; @@ -121,8 +122,8 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC constructor( private parent: HTMLElement, - protected accessor: IEditorGroupsAccessor, - protected groupViewer: IEditorGroupView, + protected groupsView: IEditorGroupsView, + protected groupView: IEditorGroupView, protected tabsModel: IReadonlyEditorGroupModel, @IContextMenuService protected readonly contextMenuService: IContextMenuService, @IInstantiationService protected instantiationService: IInstantiationService, @@ -159,7 +160,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC } protected createEditorActionsToolBar(container: HTMLElement): void { - const context: IEditorCommandsContext = { groupId: this.groupViewer.id }; + const context: IEditorCommandsContext = { groupId: this.groupView.id }; // Toolbar Widget @@ -173,7 +174,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC renderDropdownAsChildElement: this.renderDropdownAsChildElement, telemetrySource: 'editorPart', resetMenu: MenuId.EditorTitle, - maxNumberOfItems: 9, + overflowBehavior: { maxItems: 9, exempted: EDITOR_CORE_NAVIGATION_COMMANDS }, highlightToggledItems: true, })); @@ -191,7 +192,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC } private actionViewItemProvider(action: IAction): IActionViewItem | undefined { - const activeEditorPane = this.groupViewer.activeEditorPane; + const activeEditorPane = this.groupView.activeEditorPane; // Check Active Editor if (activeEditorPane instanceof EditorPane) { @@ -224,24 +225,24 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC // Update contexts this.contextKeyService.bufferChangeEvents(() => { - const activeEditor = this.groupViewer.activeEditor; + const activeEditor = this.groupView.activeEditor; this.resourceContext.set(EditorResourceAccessor.getOriginalUri(activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY } ?? null)); - this.editorPinnedContext.set(activeEditor ? this.groupViewer.isPinned(activeEditor) : false); - this.editorIsFirstContext.set(activeEditor ? this.groupViewer.isFirst(activeEditor) : false); - this.editorIsLastContext.set(activeEditor ? this.groupViewer.isLast(activeEditor) : false); - this.editorStickyContext.set(activeEditor ? this.groupViewer.isSticky(activeEditor) : false); + this.editorPinnedContext.set(activeEditor ? this.groupView.isPinned(activeEditor) : false); + this.editorIsFirstContext.set(activeEditor ? this.groupView.isFirst(activeEditor) : false); + this.editorIsLastContext.set(activeEditor ? this.groupView.isLast(activeEditor) : false); + this.editorStickyContext.set(activeEditor ? this.groupView.isSticky(activeEditor) : false); applyAvailableEditorIds(this.editorAvailableEditorIds, activeEditor, this.editorResolverService); this.editorCanSplitInGroupContext.set(activeEditor ? activeEditor.hasCapability(EditorInputCapabilities.CanSplitInGroup) : false); this.sideBySideEditorContext.set(activeEditor?.typeId === SideBySideEditorInput.ID); - this.groupLockedContext.set(this.groupViewer.isLocked); + this.groupLockedContext.set(this.groupView.isLocked); }); // Editor actions require the editor control to be there, so we retrieve it via service - const activeEditorPane = this.groupViewer.activeEditorPane; + const activeEditorPane = this.groupView.activeEditorPane; if (activeEditorPane instanceof EditorPane) { const scopedContextKeyService = this.getEditorPaneAwareContextKeyService(); const titleBarMenu = this.menuService.createMenu(MenuId.EditorTitle, scopedContextKeyService, { emitEventsForSubmenuChanges: true, eventDebounceDelay: 0 }); @@ -265,7 +266,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC } private getEditorPaneAwareContextKeyService(): IContextKeyService { - return this.groupViewer.activeEditorPane?.scopedContextKeyService ?? this.contextKeyService; + return this.groupView.activeEditorPane?.scopedContextKeyService ?? this.contextKeyService; } protected clearEditorActionsToolbar(): void { @@ -281,34 +282,34 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC } // Set editor group as transfer - this.groupTransfer.setData([new DraggedEditorGroupIdentifier(this.groupViewer.id)], DraggedEditorGroupIdentifier.prototype); + this.groupTransfer.setData([new DraggedEditorGroupIdentifier(this.groupView.id)], DraggedEditorGroupIdentifier.prototype); if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'copyMove'; } // Drag all tabs of the group if tabs are enabled let hasDataTransfer = false; - if (this.accessor.partOptions.showTabs) { - hasDataTransfer = this.doFillResourceDataTransfers(this.groupViewer.getEditors(EditorsOrder.SEQUENTIAL), e); + if (this.groupsView.partOptions.showTabs) { + hasDataTransfer = this.doFillResourceDataTransfers(this.groupView.getEditors(EditorsOrder.SEQUENTIAL), e); } // Otherwise only drag the active editor else { - if (this.groupViewer.activeEditor) { - hasDataTransfer = this.doFillResourceDataTransfers([this.groupViewer.activeEditor], e); + if (this.groupView.activeEditor) { + hasDataTransfer = this.doFillResourceDataTransfers([this.groupView.activeEditor], e); } } // Firefox: requires to set a text data transfer to get going if (!hasDataTransfer && isFirefox) { - e.dataTransfer?.setData(DataTransfers.TEXT, String(this.groupViewer.label)); + e.dataTransfer?.setData(DataTransfers.TEXT, String(this.groupView.label)); } // Drag Image - if (this.groupViewer.activeEditor) { - let label = this.groupViewer.activeEditor.getName(); - if (this.accessor.partOptions.showTabs && this.groupViewer.count > 1) { - label = localize('draggedEditorGroup', "{0} (+{1})", label, this.groupViewer.count - 1); + if (this.groupView.activeEditor) { + let label = this.groupView.activeEditor.getName(); + if (this.groupsView.partOptions.showTabs && this.groupView.count > 1) { + label = localize('draggedEditorGroup', "{0} (+{1})", label, this.groupView.count - 1); } applyDragImage(e, label, 'monaco-editor-group-drag-image', this.getColor(listActiveSelectionBackground), this.getColor(listActiveSelectionForeground)); @@ -323,7 +324,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC protected doFillResourceDataTransfers(editors: readonly EditorInput[], e: DragEvent): boolean { if (editors.length) { - this.instantiationService.invokeFunction(fillEditorsDragData, editors.map(editor => ({ editor, groupId: this.groupViewer.id })), e); + this.instantiationService.invokeFunction(fillEditorsDragData, editors.map(editor => ({ editor, groupId: this.groupView.id })), e); return true; } @@ -365,7 +366,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC menuId: MenuId.EditorTitleContext, menuActionOptions: { shouldForwardArgs: true, arg: this.resourceContext.get() }, contextKeyService: this.contextKeyService, - getActionsContext: () => ({ groupId: this.groupViewer.id, editorIndex: this.groupViewer.getIndexOfEditor(editor) }), + getActionsContext: () => ({ groupId: this.groupView.id, editorIndex: this.groupView.getIndexOfEditor(editor) }), getKeyBinding: action => this.getKeybinding(action), onHide: () => { @@ -381,7 +382,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC this.editorAvailableEditorIds.set(currentEditorAvailableEditorIds); // restore focus to active group - this.accessor.activeGroup.focus(); + this.groupsView.activeGroup.focus(); } }); } @@ -397,7 +398,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC } protected get tabHeight() { - return this.accessor.partOptions.tabHeight !== 'compact' ? EditorTabsControl.EDITOR_TAB_HEIGHT.normal : EditorTabsControl.EDITOR_TAB_HEIGHT.compact; + return this.groupsView.partOptions.tabHeight !== 'compact' ? EditorTabsControl.EDITOR_TAB_HEIGHT.normal : EditorTabsControl.EDITOR_TAB_HEIGHT.compact; } protected updateTabHeight(): void { diff --git a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts index dc3b68698ad..f136e7572e2 100644 --- a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts @@ -8,7 +8,7 @@ import { Dimension, clearNode } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { BreadcrumbsControl, BreadcrumbsControlFactory } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; -import { IEditorGroupsAccessor, IEditorGroupTitleHeight, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, IEditorGroupTitleHeight, IEditorGroupView, IInternalEditorOpenOptions } from 'vs/workbench/browser/parts/editor/editor'; import { IEditorTabsControl } from 'vs/workbench/browser/parts/editor/editorTabsControl'; import { MultiEditorTabsControl } from 'vs/workbench/browser/parts/editor/multiEditorTabsControl'; import { SingleEditorTabsControl } from 'vs/workbench/browser/parts/editor/singleEditorTabsControl'; @@ -43,8 +43,8 @@ export class EditorTitleControl extends Themable { constructor( private parent: HTMLElement, - private accessor: IEditorGroupsAccessor, - private groupViewer: IEditorGroupView, + private groupsView: IEditorGroupsView, + private groupView: IEditorGroupView, private model: IReadonlyEditorGroupModel, @IInstantiationService private instantiationService: IInstantiationService, @IThemeService themeService: IThemeService @@ -57,21 +57,21 @@ export class EditorTitleControl extends Themable { private createEditorTabsControl(): IEditorTabsControl { let control: IEditorTabsControl; - if (this.accessor.partOptions.showTabs) { - if (this.accessor.partOptions.pinnedTabsOnSeparateRow) { - control = this.instantiationService.createInstance(MultiRowEditorControl, this.parent, this.accessor, this.groupViewer, this.model); + if (this.groupsView.partOptions.showTabs) { + if (this.groupsView.partOptions.pinnedTabsOnSeparateRow) { + control = this.instantiationService.createInstance(MultiRowEditorControl, this.parent, this.groupsView, this.groupView, this.model); } else { - control = this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.accessor, this.groupViewer, this.model); + control = this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.groupsView, this.groupView, this.model); } } else { - control = this.instantiationService.createInstance(SingleEditorTabsControl, this.parent, this.accessor, this.groupViewer, this.model); + control = this.instantiationService.createInstance(SingleEditorTabsControl, this.parent, this.groupsView, this.groupView, this.model); } return this.editorTabsControlDisposable.add(control); } private createBreadcrumbsControl(): BreadcrumbsControlFactory | undefined { - if (!this.accessor.partOptions.showTabs) { + if (!this.groupsView.partOptions.showTabs) { return undefined; // single tabs have breadcrumbs inlined } @@ -80,7 +80,7 @@ export class EditorTitleControl extends Themable { breadcrumbsContainer.classList.add('breadcrumbs-below-tabs'); this.parent.appendChild(breadcrumbsContainer); - const breadcrumbsControlFactory = this.breadcrumbsControlDisposables.add(this.instantiationService.createInstance(BreadcrumbsControlFactory, breadcrumbsContainer, this.groupViewer, { + const breadcrumbsControlFactory = this.breadcrumbsControlDisposables.add(this.instantiationService.createInstance(BreadcrumbsControlFactory, breadcrumbsContainer, this.groupView, { showFileIcons: true, showSymbolIcons: true, showDecorationColors: false, @@ -92,11 +92,11 @@ export class EditorTitleControl extends Themable { } private handleBreadcrumbsEnablementChange(): void { - this.groupViewer.relayout(); // relayout when breadcrumbs are enable/disabled + this.groupView.relayout(); // relayout when breadcrumbs are enable/disabled } - openEditor(editor: EditorInput): void { - const didChange = this.editorTabsControl.openEditor(editor); + openEditor(editor: EditorInput, options?: IInternalEditorOpenOptions): void { + const didChange = this.editorTabsControl.openEditor(editor, options); this.handleOpenedEditors(didChange); } @@ -132,7 +132,7 @@ export class EditorTitleControl extends Themable { } private handleClosedEditors(): void { - if (!this.groupViewer.activeEditor) { + if (!this.groupView.activeEditor) { this.breadcrumbsControl?.update(); } } diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 6073b594f0d..307d4d30f63 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -34,7 +34,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { MergeGroupMode, IMergeGroupOptions, GroupsArrangement, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; -import { IEditorGroupsAccessor, EditorServiceImpl, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { assertAllDefined, assertIsDefined } from 'vs/base/common/types'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -134,8 +134,8 @@ export class MultiEditorTabsControl extends EditorTabsControl { constructor( parent: HTMLElement, - accessor: IEditorGroupsAccessor, - groupViewer: IEditorGroupView, + groupsView: IEditorGroupsView, + groupView: IEditorGroupView, tabsModel: IReadonlyEditorGroupModel, @IContextMenuService contextMenuService: IContextMenuService, @IInstantiationService instantiationService: IInstantiationService, @@ -151,7 +151,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { @ITreeViewsDnDService private readonly treeViewsDragAndDropService: ITreeViewsDnDService, @IEditorResolverService editorResolverService: IEditorResolverService ) { - super(parent, accessor, groupViewer, tabsModel, contextMenuService, instantiationService, contextKeyService, keybindingService, notificationService, menuService, quickInputService, themeService, editorResolverService); + super(parent, groupsView, groupView, tabsModel, contextMenuService, instantiationService, contextKeyService, keybindingService, notificationService, menuService, quickInputService, themeService, editorResolverService); // Resolve the correct path library for the OS we are on // If we are connected to remote, this accounts for the @@ -230,7 +230,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { tabSizingFixedDisposables.clear(); - const options = this.accessor.partOptions; + const options = this.groupsView.partOptions; if (options.tabSizing === 'fixed') { tabsContainer.style.setProperty('--tab-sizing-fixed-min-width', `${options.tabSizingFixedMinWidth}px`); tabsContainer.style.setProperty('--tab-sizing-fixed-max-width', `${options.tabSizingFixedMaxWidth}px`); @@ -266,7 +266,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } private getTabsScrollbarSizing(): number { - if (this.accessor.partOptions.titleScrollbarSizing !== 'large') { + if (this.groupsView.partOptions.titleScrollbarSizing !== 'large') { return MultiEditorTabsControl.SCROLLBAR_SIZES.default; } @@ -310,10 +310,10 @@ export class MultiEditorTabsControl extends EditorTabsControl { resource: undefined, options: { pinned: true, - index: this.groupViewer.count, // always at the end + index: this.groupView.count, // always at the end override: DEFAULT_EDITOR_ASSOCIATION.id } - }, this.groupViewer.id); + }, this.groupView.id); })); } @@ -354,7 +354,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); if (Array.isArray(data)) { const localDraggedEditor = data[0].identifier; - if (this.groupViewer.id === localDraggedEditor.groupId && this.tabsModel.isLast(localDraggedEditor.editor)) { + if (this.groupView.id === localDraggedEditor.groupId && this.tabsModel.isLast(localDraggedEditor.editor)) { if (e.dataTransfer) { e.dataTransfer.dropEffect = 'none'; } @@ -397,13 +397,13 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Mouse-wheel support to switch to tabs optionally this._register(addDisposableListener(tabsContainer, EventType.MOUSE_WHEEL, (e: WheelEvent) => { - const activeEditor = this.groupViewer.activeEditor; - if (!activeEditor || this.groupViewer.count < 2) { + const activeEditor = this.groupView.activeEditor; + if (!activeEditor || this.groupView.count < 2) { return; // need at least 2 open editors } // Shift-key enables or disables this behaviour depending on the setting - if (this.accessor.partOptions.scrollToSwitchTabs === true) { + if (this.groupsView.partOptions.scrollToSwitchTabs === true) { if (e.shiftKey) { return; // 'on': only enable this when Shift-key is not pressed } @@ -433,13 +433,13 @@ export class MultiEditorTabsControl extends EditorTabsControl { return; } - const nextEditor = this.groupViewer.getEditorByIndex(this.groupViewer.getIndexOfEditor(activeEditor) + tabSwitchDirection); + const nextEditor = this.groupView.getEditorByIndex(this.groupView.getIndexOfEditor(activeEditor) + tabSwitchDirection); if (!nextEditor) { return; } // Open it - this.groupViewer.openEditor(nextEditor); + this.groupView.openEditor(nextEditor); // Disable normal scrolling, opening the editor will already reveal it properly EventHelper.stop(e, true); @@ -461,9 +461,9 @@ export class MultiEditorTabsControl extends EditorTabsControl { menuId: MenuId.EditorTabsBarContext, contextKeyService: this.contextKeyService, menuActionOptions: { shouldForwardArgs: true }, - getActionsContext: () => ({ groupId: this.groupViewer.id }), + getActionsContext: () => ({ groupId: this.groupView.id }), getKeyBinding: action => this.getKeybinding(action), - onHide: () => this.groupViewer.focus() + onHide: () => this.groupView.focus() }); }; @@ -486,8 +486,15 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.layout(this.dimensions); } - openEditor(editor: EditorInput): boolean { - return this.handleOpenedEditors(); + openEditor(editor: EditorInput, options?: IInternalEditorOpenOptions): boolean { + const changed = this.handleOpenedEditors(); + + // Respect option to focus tab control if provided + if (options?.focusTabControl) { + this.withTab(editor, (editor, tabIndex, tabContainer) => tabContainer.focus()); + } + + return changed; } openEditors(editors: EditorInput[]): boolean { @@ -568,7 +575,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // This helps keeping the close button stable under // the mouse and allows for rapid closing of tabs. - if (this.isMouseOverTabs && this.accessor.partOptions.tabSizing === 'fixed') { + if (this.isMouseOverTabs && this.groupsView.partOptions.tabSizing === 'fixed') { const closingLastTab = this.tabsModel.isLast(editor); this.updateTabsFixedWidth(!closingLastTab); } @@ -707,7 +714,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } updateEditorDirty(editor: EditorInput): void { - this.withTab(editor, (editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => this.redrawTabActiveAndDirty(this.accessor.activeGroup === this.groupViewer, editor, tabContainer, tabActionBar)); + this.withTab(editor, (editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => this.redrawTabActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar)); } override updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { @@ -807,12 +814,8 @@ export class MultiEditorTabsControl extends EditorTabsControl { const that = this; const tabActionRunner = new EditorCommandsContextActionRunner({ - groupId: this.groupViewer.id, - get editorIndex() { - const editor = assertIsDefined(that.tabsModel.getEditorByIndex(tabIndex)); - - return that.groupViewer.getIndexOfEditor(editor); - }, + groupId: this.groupView.id, + get editorIndex() { return that.toEditorIndex(tabIndex); } }); const tabActionBar = new ActionBar(tabActionsContainer, { ariaLabel: localize('ariaLabelTabActions', "Tab actions"), actionRunner: tabActionRunner }); @@ -837,6 +840,16 @@ export class MultiEditorTabsControl extends EditorTabsControl { return tabContainer; } + private toEditorIndex(tabIndex: number): number { + + // Given a `tabIndex` that is relative to the tabs model + // returns the `editorIndex` relative to the entire group + + const editor = assertIsDefined(this.tabsModel.getEditorByIndex(tabIndex)); + + return this.groupView.getIndexOfEditor(editor); + } + private registerTabListeners(tab: HTMLElement, tabIndex: number, tabsContainer: HTMLElement, tabsScrollbar: ScrollableElement): IDisposable { const disposables = new DisposableStore(); @@ -859,7 +872,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { const editor = this.tabsModel.getEditorByIndex(tabIndex); if (editor) { // Even if focus is preserved make sure to activate the group. - this.groupViewer.openEditor(editor, { preserveFocus, activation: EditorActivation.ACTIVATE }); + this.groupView.openEditor(editor, { preserveFocus, activation: EditorActivation.ACTIVATE }); } return undefined; @@ -897,12 +910,12 @@ export class MultiEditorTabsControl extends EditorTabsControl { const editor = this.tabsModel.getEditorByIndex(tabIndex); if (editor) { - if (preventEditorClose(this.tabsModel, editor, EditorCloseMethod.MOUSE, this.accessor.partOptions)) { + if (preventEditorClose(this.tabsModel, editor, EditorCloseMethod.MOUSE, this.groupsView.partOptions)) { return; } this.blockRevealActiveTabOnce(); - this.closeEditorAction.run({ groupId: this.groupViewer.id, editorIndex: this.groupViewer.getIndexOfEditor(editor) }); + this.closeEditorAction.run({ groupId: this.groupView.id, editorIndex: this.groupView.getIndexOfEditor(editor) }); } } })); @@ -930,28 +943,27 @@ export class MultiEditorTabsControl extends EditorTabsControl { handled = true; const editor = this.tabsModel.getEditorByIndex(tabIndex); if (editor) { - this.groupViewer.openEditor(editor); + this.groupView.openEditor(editor); } } // Navigate in editors else if ([KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.Home, KeyCode.End].some(kb => event.equals(kb))) { - let tabTargetIndex: number; + let editorIndex = this.toEditorIndex(tabIndex); if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.UpArrow)) { - tabTargetIndex = tabIndex - 1; + editorIndex = editorIndex - 1; } else if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.DownArrow)) { - tabTargetIndex = tabIndex + 1; + editorIndex = editorIndex + 1; } else if (event.equals(KeyCode.Home)) { - tabTargetIndex = 0; + editorIndex = 0; } else { - tabTargetIndex = this.tabsModel.count - 1; + editorIndex = this.groupView.count - 1; } - const target = this.tabsModel.getEditorByIndex(tabTargetIndex); + const target = this.groupView.getEditorByIndex(editorIndex); if (target) { handled = true; - this.groupViewer.openEditor(target, { preserveFocus: true }); - (tabsContainer.childNodes[tabTargetIndex]).focus(); + this.groupView.openEditor(target, { preserveFocus: true }, { focusTabControl: true }); } } @@ -976,11 +988,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { const editor = this.tabsModel.getEditorByIndex(tabIndex); if (editor && this.tabsModel.isPinned(editor)) { - if (this.accessor.partOptions.doubleClickTabToToggleEditorGroupSizes) { - this.accessor.arrangeGroups(GroupsArrangement.TOGGLE, this.groupViewer); + if (this.groupsView.partOptions.doubleClickTabToToggleEditorGroupSizes) { + this.groupsView.arrangeGroups(GroupsArrangement.TOGGLE, this.groupView); } } else { - this.groupViewer.pinEditor(editor); + this.groupView.pinEditor(editor); } })); } @@ -1002,7 +1014,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { return; } - this.editorTransfer.setData([new DraggedEditorIdentifier({ editor, groupId: this.groupViewer.id })], DraggedEditorIdentifier.prototype); + this.editorTransfer.setData([new DraggedEditorIdentifier({ editor, groupId: this.groupView.id })], DraggedEditorIdentifier.prototype); if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'copyMove'; @@ -1040,7 +1052,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); if (Array.isArray(data)) { const localDraggedEditor = data[0].identifier; - if (localDraggedEditor.editor === this.tabsModel.getEditorByIndex(tabIndex) && localDraggedEditor.groupId === this.groupViewer.id) { + if (localDraggedEditor.editor === this.tabsModel.getEditorByIndex(tabIndex) && localDraggedEditor.groupId === this.groupView.id) { if (e.dataTransfer) { e.dataTransfer.dropEffect = 'none'; } @@ -1065,7 +1077,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { if (dragDuration >= MultiEditorTabsControl.DRAG_OVER_OPEN_TAB_THRESHOLD) { const draggedOverTab = this.tabsModel.getEditorByIndex(tabIndex); if (draggedOverTab && this.tabsModel.activeEditor !== draggedOverTab) { - this.groupViewer.openEditor(draggedOverTab, { preserveFocus: true }); + this.groupView.openEditor(draggedOverTab, { preserveFocus: true }); } } }, @@ -1098,7 +1110,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); if (Array.isArray(data)) { const group = data[0]; - if (group.identifier === this.groupViewer.id) { + if (group.identifier === this.groupView.id) { return false; // groups cannot be dropped on group it originates from } } @@ -1142,7 +1154,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } private computeTabLabels(): void { - const { labelFormat } = this.accessor.partOptions; + const { labelFormat } = this.groupsView.partOptions; const { verbosity, shortenDuplicates } = this.getLabelConfigFlags(labelFormat); // Build labels and descriptions for each editor @@ -1155,7 +1167,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { description: editor.getDescription(verbosity), forceDescription: editor.hasCapability(EditorInputCapabilities.ForceDescription), title: editor.getTitle(Verbosity.LONG), - ariaLabel: computeEditorAriaLabel(editor, tabIndex, this.groupViewer, this.editorGroupService.count) + ariaLabel: computeEditorAriaLabel(editor, tabIndex, this.groupView, this.editorGroupService.count) }); if (editor === this.tabsModel.activeEditor) { @@ -1292,7 +1304,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { private redrawTab(editor: EditorInput, tabIndex: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar): void { const isTabSticky = this.tabsModel.isSticky(tabIndex); - const options = this.accessor.partOptions; + const options = this.groupsView.partOptions; // Label this.redrawTabLabel(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel); @@ -1347,11 +1359,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.redrawTabBorders(tabIndex, tabContainer); // Active / dirty state - this.redrawTabActiveAndDirty(this.accessor.activeGroup === this.groupViewer, editor, tabContainer, tabActionBar); + this.redrawTabActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar); } private redrawTabLabel(editor: EditorInput, tabIndex: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void { - const options = this.accessor.partOptions; + const options = this.groupsView.partOptions; // Unless tabs are sticky compact, show the full label and description // Sticky compact tabs will only show an icon if icons are enabled @@ -1474,7 +1486,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { tabContainer.classList.add('dirty'); // Highlight modified tabs with a border if configured - if (this.accessor.partOptions.highlightModifiedTabs) { + if (this.groupsView.partOptions.highlightModifiedTabs) { let modifiedBorderColor: string | null; if (isGroupActive && isTabActive) { modifiedBorderColor = this.getColor(TAB_ACTIVE_MODIFIED_BORDER); @@ -1510,15 +1522,16 @@ export class MultiEditorTabsControl extends EditorTabsControl { private redrawTabBorders(tabIndex: number, tabContainer: HTMLElement): void { const isTabSticky = this.tabsModel.isSticky(tabIndex); const isTabLastSticky = isTabSticky && this.tabsModel.stickyCount === tabIndex + 1; + const showLastStickyTabBorderColor = this.tabsModel.stickyCount !== this.tabsModel.count; // Borders / Outline - const borderRightColor = ((isTabLastSticky ? this.getColor(TAB_LAST_PINNED_BORDER) : undefined) || this.getColor(TAB_BORDER) || this.getColor(contrastBorder)); + const borderRightColor = ((isTabLastSticky && showLastStickyTabBorderColor ? this.getColor(TAB_LAST_PINNED_BORDER) : undefined) || this.getColor(TAB_BORDER) || this.getColor(contrastBorder)); tabContainer.style.borderRight = borderRightColor ? `1px solid ${borderRightColor}` : ''; tabContainer.style.outlineColor = this.getColor(activeContrastBorder) || ''; } protected override prepareEditorActions(editorActions: IToolbarActions): IToolbarActions { - const isGroupActive = this.accessor.activeGroup === this.groupViewer; + const isGroupActive = this.groupsView.activeGroup === this.groupView; // Active: allow all actions if (isGroupActive) { @@ -1552,7 +1565,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { if (!this.visible) { height = 0; - } else if (this.accessor.partOptions.wrapTabs && this.tabsAndActionsContainer?.classList.contains('wrapping')) { + } else if (this.groupsView.partOptions.wrapTabs && this.tabsAndActionsContainer?.classList.contains('wrapping')) { // Wrap: we need to ask `offsetHeight` to get // the real height of the title area with wrapping. height = this.tabsAndActionsContainer.offsetHeight; @@ -1617,7 +1630,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // to signal this to the outside via a `relayout` call so that // e.g. the editor control can be adjusted accordingly. if (oldDimension && oldDimension.height !== newDimension.height) { - this.groupViewer.relayout(); + this.groupView.relayout(); } } @@ -1657,7 +1670,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } // Setting enabled: selectively enable wrapping if possible - if (this.accessor.partOptions.wrapTabs) { + if (this.groupsView.partOptions.wrapTabs) { const visibleTabsWidth = tabsContainer.offsetWidth; const allTabsWidth = tabsContainer.scrollWidth; const lastTabFitsWrapped = () => { @@ -1792,7 +1805,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { let stickyTabsWidth = 0; if (this.tabsModel.stickyCount > 0) { let stickyTabWidth = 0; - switch (this.accessor.partOptions.pinnedTabSizing) { + switch (this.groupsView.partOptions.pinnedTabSizing) { case 'compact': stickyTabWidth = MultiEditorTabsControl.TAB_WIDTH.compact; break; @@ -1809,7 +1822,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Figure out if active tab is positioned static which has an // impact on whether to reveal the tab or not later - let activeTabPositionStatic = this.accessor.partOptions.pinnedTabSizing !== 'normal' && typeof activeTabIndex === 'number' && this.tabsModel.isSticky(activeTabIndex); + let activeTabPositionStatic = this.groupsView.partOptions.pinnedTabSizing !== 'normal' && typeof activeTabIndex === 'number' && this.tabsModel.isSticky(activeTabIndex); // Special case: we have sticky tabs but the available space for showing tabs // is little enough that we need to disable sticky tabs sticky positioning @@ -1972,7 +1985,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.updateDropFeedback(tabsContainer, false); tabsContainer.classList.remove('scroll'); - const targetEditorIndex = this.tabsModel instanceof UnstickyEditorGroupModel ? targetTabIndex + this.groupViewer.stickyCount : targetTabIndex; + const targetEditorIndex = this.tabsModel instanceof UnstickyEditorGroupModel ? targetTabIndex + this.groupView.stickyCount : targetTabIndex; const options: IEditorOptions = { sticky: this.tabsModel instanceof StickyEditorGroupModel && this.tabsModel.stickyCount === targetEditorIndex, index: targetEditorIndex @@ -1982,17 +1995,17 @@ export class MultiEditorTabsControl extends EditorTabsControl { if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); if (Array.isArray(data)) { - const sourceGroup = this.accessor.getGroup(data[0].identifier); + const sourceGroup = this.groupsView.getGroup(data[0].identifier); if (sourceGroup) { const mergeGroupOptions: IMergeGroupOptions = { index: targetEditorIndex }; if (!this.isMoveOperation(e, sourceGroup.id)) { mergeGroupOptions.mode = MergeGroupMode.COPY_EDITORS; } - this.accessor.mergeGroup(sourceGroup, this.groupViewer, mergeGroupOptions); + this.groupsView.mergeGroup(sourceGroup, this.groupView, mergeGroupOptions); } - this.groupViewer.focus(); + this.groupView.focus(); this.groupTransfer.clearData(DraggedEditorGroupIdentifier.prototype); } } @@ -2002,21 +2015,21 @@ export class MultiEditorTabsControl extends EditorTabsControl { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); if (Array.isArray(data)) { const draggedEditor = data[0].identifier; - const sourceGroup = this.accessor.getGroup(draggedEditor.groupId); + const sourceGroup = this.groupsView.getGroup(draggedEditor.groupId); if (sourceGroup) { // Move editor to target position and index if (this.isMoveOperation(e, draggedEditor.groupId, draggedEditor.editor)) { - sourceGroup.moveEditor(draggedEditor.editor, this.groupViewer, options); + sourceGroup.moveEditor(draggedEditor.editor, this.groupView, options); } // Copy editor to target position and index else { - sourceGroup.copyEditor(draggedEditor.editor, this.groupViewer, options); + sourceGroup.copyEditor(draggedEditor.editor, this.groupView, options); } } - this.groupViewer.focus(); + this.groupView.focus(); this.editorTransfer.clearData(DraggedEditorIdentifier.prototype); } } @@ -2034,7 +2047,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } } - this.editorService.openEditors(editors, this.groupViewer, { validateTrust: true }); + this.editorService.openEditors(editors, this.groupView, { validateTrust: true }); } this.treeItemsTransfer.clearData(DraggedTreeItemsIdentifier.prototype); @@ -2043,7 +2056,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Check for URI transfer else { const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: false }); - dropHandler.handleDrop(e, () => this.groupViewer, () => this.groupViewer.focus(), options); + dropHandler.handleDrop(e, () => this.groupView, () => this.groupView.focus(), options); } } @@ -2054,7 +2067,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { const isCopy = (e.ctrlKey && !isMacintosh) || (e.altKey && isMacintosh); - return (!isCopy || sourceGroup === this.groupViewer.id); + return (!isCopy || sourceGroup === this.groupView.id); } override dispose(): void { diff --git a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts index ef552ff9a96..82ee1d6d9b2 100644 --- a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -5,7 +5,7 @@ import { Dimension } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, IEditorGroupView, IInternalEditorOpenOptions } from 'vs/workbench/browser/parts/editor/editor'; import { IEditorTabsControl } from 'vs/workbench/browser/parts/editor/editorTabsControl'; import { MultiEditorTabsControl } from 'vs/workbench/browser/parts/editor/multiEditorTabsControl'; import { IEditorPartOptions } from 'vs/workbench/common/editor'; @@ -22,8 +22,8 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont constructor( private parent: HTMLElement, - private accessor: IEditorGroupsAccessor, - private groupViewer: IEditorGroupView, + private groupsView: IEditorGroupsView, + private groupView: IEditorGroupView, private model: IReadonlyEditorGroupModel, @IInstantiationService protected instantiationService: IInstantiationService ) { @@ -32,19 +32,19 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont const stickyModel = this._register(new StickyEditorGroupModel(this.model)); const unstickyModel = this._register(new UnstickyEditorGroupModel(this.model)); - this.stickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.accessor, this.groupViewer, stickyModel)); - this.unstickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.accessor, this.groupViewer, unstickyModel)); + this.stickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.groupsView, this.groupView, stickyModel)); + this.unstickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.groupsView, this.groupView, unstickyModel)); this.handlePinnedTabsSeparateRowToolbars(); } private handlePinnedTabsSeparateRowToolbars(): void { - if (this.groupViewer.count === 0) { + if (this.groupView.count === 0) { // Do nothing as no tab bar is visible return; } // Ensure action toolbar is only visible once - if (this.groupViewer.count === this.groupViewer.stickyCount) { + if (this.groupView.count === this.groupView.stickyCount) { this.parent.classList.toggle('two-tab-bars', false); } else { this.parent.classList.toggle('two-tab-bars', true); @@ -55,9 +55,9 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont return this.model.isSticky(editor) ? this.stickyEditorTabsControl : this.unstickyEditorTabsControl; } - openEditor(editor: EditorInput): boolean { + openEditor(editor: EditorInput, options: IInternalEditorOpenOptions): boolean { const [editorTabController, otherTabController] = this.model.isSticky(editor) ? [this.stickyEditorTabsControl, this.unstickyEditorTabsControl] : [this.unstickyEditorTabsControl, this.stickyEditorTabsControl]; - const didChange = editorTabController.openEditor(editor); + const didChange = editorTabController.openEditor(editor, options); if (didChange) { // HACK: To render all editor tabs on startup, otherwise only one row gets rendered otherTabController.openEditors([]); diff --git a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts index 5cf2d6e27ef..8efda41b36d 100644 --- a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts @@ -55,7 +55,7 @@ export class SingleEditorTabsControl extends EditorTabsControl { this._register(addDisposableListener(this.editorLabel.element, EventType.CLICK, e => this.onTitleLabelClick(e))); // Breadcrumbs - this.breadcrumbsControlFactory = this._register(this.instantiationService.createInstance(BreadcrumbsControlFactory, labelContainer, this.groupViewer, { + this.breadcrumbsControlFactory = this._register(this.instantiationService.createInstance(BreadcrumbsControlFactory, labelContainer, this.groupView, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, @@ -109,15 +109,15 @@ export class SingleEditorTabsControl extends EditorTabsControl { private onTitleDoubleClick(e: MouseEvent): void { EventHelper.stop(e); - this.groupViewer.pinEditor(); + this.groupView.pinEditor(); } private onTitleAuxClick(e: MouseEvent): void { if (e.button === 1 /* Middle Button */ && this.tabsModel.activeEditor) { EventHelper.stop(e, true /* for https://github.com/microsoft/vscode/issues/56715 */); - if (!preventEditorClose(this.tabsModel, this.tabsModel.activeEditor, EditorCloseMethod.MOUSE, this.accessor.partOptions)) { - this.groupViewer.closeEditor(this.tabsModel.activeEditor); + if (!preventEditorClose(this.tabsModel, this.tabsModel.activeEditor, EditorCloseMethod.MOUSE, this.groupsView.partOptions)) { + this.groupView.closeEditor(this.tabsModel.activeEditor); } } } @@ -261,10 +261,10 @@ export class SingleEditorTabsControl extends EditorTabsControl { private redraw(): void { const editor = this.tabsModel.activeEditor ?? undefined; - const options = this.accessor.partOptions; + const options = this.groupsView.partOptions; const isEditorPinned = editor ? this.tabsModel.isPinned(editor) : false; - const isGroupActive = this.accessor.activeGroup === this.groupViewer; + const isGroupActive = this.groupsView.activeGroup === this.groupView; this.activeLabel = { editor, pinned: isEditorPinned }; @@ -293,7 +293,7 @@ export class SingleEditorTabsControl extends EditorTabsControl { this.updateEditorDirty(editor); // Editor Label - const { labelFormat } = this.accessor.partOptions; + const { labelFormat } = this.groupsView.partOptions; let description: string; if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { description = ''; // hide description when showing breadcrumbs @@ -345,7 +345,7 @@ export class SingleEditorTabsControl extends EditorTabsControl { } protected override prepareEditorActions(editorActions: IToolbarActions): IToolbarActions { - const isGroupActive = this.accessor.activeGroup === this.groupViewer; + const isGroupActive = this.groupsView.activeGroup === this.groupView; // Active: allow all actions if (isGroupActive) { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 6d02c45b4ca..e6c8feb8e52 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -93,7 +93,6 @@ export function registerNotificationCommands(center: INotificationsCenterControl KeybindingsRegistry.registerCommandAndKeybindingRule({ id: SHOW_NOTIFICATIONS_CENTER, weight: KeybindingWeight.WorkbenchContrib, - when: NotificationsCenterVisibleContext.negate(), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyN), handler: () => { toasts.hide(); diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 24231e4dd00..adacb04b693 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -1834,4 +1834,6 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop { this.dragCancellationToken?.cancel(); } } + + dispose(): void { } } diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index e394038fb85..fc4bb668d21 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -174,14 +174,14 @@ export interface IReadonlyEditorGroupModel { readonly activeEditor: EditorInput | null; readonly previewEditor: EditorInput | null; - getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): readonly EditorInput[]; + getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): EditorInput[]; getEditorByIndex(index: number): EditorInput | undefined; indexOf(editor: EditorInput | IUntypedEditorInput | null, editors?: EditorInput[], options?: IMatchEditorOptions): number; isActive(editor: EditorInput | IUntypedEditorInput): boolean; isPinned(editorOrIndex: EditorInput | number): boolean; isSticky(editorOrIndex: EditorInput | number): boolean; - isFirst(editor: EditorInput): boolean; - isLast(editor: EditorInput): boolean; + isFirst(editor: EditorInput, editors?: EditorInput[]): boolean; + isLast(editor: EditorInput, editors?: EditorInput[]): boolean; findEditor(editor: EditorInput | null, options?: IMatchEditorOptions): [EditorInput, number /* index */] | undefined; contains(editor: EditorInput | IUntypedEditorInput, options?: IMatchEditorOptions): boolean; } @@ -937,19 +937,19 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { return [this.editors[index], index]; } - isFirst(candidate: EditorInput | null): boolean { - return this.matches(this.editors[0], candidate); + isFirst(candidate: EditorInput | null, editors = this.editors): boolean { + return this.matches(editors[0], candidate); } - isLast(candidate: EditorInput | null): boolean { - return this.matches(this.editors[this.editors.length - 1], candidate); + isLast(candidate: EditorInput | null, editors = this.editors): boolean { + return this.matches(editors[editors.length - 1], candidate); } contains(candidate: EditorInput | IUntypedEditorInput, options?: IMatchEditorOptions): boolean { return this.indexOf(candidate, this.editors, options) !== -1; } - private matches(editor: EditorInput | null, candidate: EditorInput | IUntypedEditorInput | null, options?: IMatchEditorOptions): boolean { + private matches(editor: EditorInput | null | undefined, candidate: EditorInput | IUntypedEditorInput | null, options?: IMatchEditorOptions): boolean { if (!editor || !candidate) { return false; } diff --git a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts index a8dc47321d1..7b427fe5ded 100644 --- a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts +++ b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts @@ -41,7 +41,15 @@ abstract class FilteredEditorGroupModel extends Disposable implements IReadonlyE isSticky(editorOrIndex: EditorInput | number): boolean { return this.model.isSticky(editorOrIndex); } isActive(editor: EditorInput | IUntypedEditorInput): boolean { return this.model.isActive(editor); } - getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): readonly EditorInput[] { + isFirst(editor: EditorInput): boolean { + return this.model.isFirst(editor, this.getEditors(EditorsOrder.SEQUENTIAL)); + } + + isLast(editor: EditorInput): boolean { + return this.model.isLast(editor, this.getEditors(EditorsOrder.SEQUENTIAL)); + } + + getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): EditorInput[] { const editors = this.model.getEditors(order, options); return editors.filter(e => this.filter(e)); } @@ -56,8 +64,6 @@ abstract class FilteredEditorGroupModel extends Disposable implements IReadonlyE abstract get count(): number; - abstract isFirst(editor: EditorInput): boolean; - abstract isLast(editor: EditorInput): boolean; abstract getEditorByIndex(index: number): EditorInput | undefined; abstract indexOf(editor: EditorInput | IUntypedEditorInput | null, editors?: EditorInput[], options?: IMatchEditorOptions): number; abstract contains(editor: EditorInput | IUntypedEditorInput, options?: IMatchEditorOptions): boolean; @@ -68,7 +74,7 @@ abstract class FilteredEditorGroupModel extends Disposable implements IReadonlyE export class StickyEditorGroupModel extends FilteredEditorGroupModel { get count(): number { return this.model.stickyCount; } - override getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): readonly EditorInput[] { + override getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): EditorInput[] { if (options?.excludeSticky) { return []; } @@ -82,14 +88,6 @@ export class StickyEditorGroupModel extends FilteredEditorGroupModel { return true; } - isFirst(editor: EditorInput): boolean { - return this.model.isFirst(editor); - } - - isLast(editor: EditorInput): boolean { - return this.model.indexOf(editor) === this.model.stickyCount - 1; - } - getEditorByIndex(index: number): EditorInput | undefined { return index < this.count ? this.model.getEditorByIndex(index) : undefined; } @@ -120,21 +118,13 @@ export class UnstickyEditorGroupModel extends FilteredEditorGroupModel { return false; } - override getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): readonly EditorInput[] { + override getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): EditorInput[] { if (order === EditorsOrder.SEQUENTIAL) { return this.model.getEditors(EditorsOrder.SEQUENTIAL).slice(this.model.stickyCount); } return super.getEditors(order, options); } - isFirst(editor: EditorInput): boolean { - return this.model.indexOf(editor) === this.model.stickyCount; - } - - isLast(editor: EditorInput): boolean { - return this.model.isLast(editor); - } - getEditorByIndex(index: number): EditorInput | undefined { return index >= 0 ? this.model.getEditorByIndex(index + this.model.stickyCount) : undefined; } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts index 0c245548ad0..cbb74ee49b9 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts @@ -102,11 +102,14 @@ class AccessibilityHelpProvider implements IAccessibleContentProvider { const editorContext = this._contextKeyService.getContext(this._editor.getDomNode()!); if (editorContext.getValue(CommentContextKeys.activeEditorHasCommentingRange.key)) { - content.push(this._descriptionForCommand(CommentCommandId.Add, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.addCommentNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.NextThread, CommentAccessibilityHelpNLS.nextCommentThreadKb, CommentAccessibilityHelpNLS.nextCommentThreadNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.PreviousThread, CommentAccessibilityHelpNLS.previousCommentThreadKb, CommentAccessibilityHelpNLS.previousCommentThreadNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.NextRange, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.nextRangeNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.PreviousRange, CommentAccessibilityHelpNLS.previousRange, CommentAccessibilityHelpNLS.previousRangeNoKb)); + const commentCommandInfo = []; + commentCommandInfo.push(CommentAccessibilityHelpNLS.intro); + commentCommandInfo.push(this._descriptionForCommand(CommentCommandId.Add, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.addCommentNoKb)); + commentCommandInfo.push(this._descriptionForCommand(CommentCommandId.NextThread, CommentAccessibilityHelpNLS.nextCommentThreadKb, CommentAccessibilityHelpNLS.nextCommentThreadNoKb)); + commentCommandInfo.push(this._descriptionForCommand(CommentCommandId.PreviousThread, CommentAccessibilityHelpNLS.previousCommentThreadKb, CommentAccessibilityHelpNLS.previousCommentThreadNoKb)); + commentCommandInfo.push(this._descriptionForCommand(CommentCommandId.NextRange, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.nextRangeNoKb)); + commentCommandInfo.push(this._descriptionForCommand(CommentCommandId.PreviousRange, CommentAccessibilityHelpNLS.previousRange, CommentAccessibilityHelpNLS.previousRangeNoKb)); + content.push(commentCommandInfo.join('\n')); } if (options.get(EditorOption.stickyScroll).enabled) { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 568d0719d21..e93c7388477 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -363,7 +363,7 @@ export class AccessibleView extends Disposable { this._accessibleViewCurrentProviderId.set(provider.verbositySettingKey.replaceAll('accessibility.verbosity.', '')); } const value = this._configurationService.getValue(provider.verbositySettingKey); - const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\n\nPress H now to open a browser window with more information related to accessibility.\n\n") : ''; + const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\n\nOpen a browser window with more information related to accessibility (H).") : ''; let disableHelpHint = ''; if (provider.options.type === AccessibleViewType.Help && !!value) { disableHelpHint = this._getDisableVerbosityHint(provider.verbositySettingKey); @@ -384,7 +384,8 @@ export class AccessibleView extends Disposable { message += '\n'; } } - this._currentContent = message + provider.provideContent() + readMoreLink + disableHelpHint; + const exitThisDialogHint = localize('exit', '\n\nExit this dialog (Escape).'); + this._currentContent = message + provider.provideContent() + readMoreLink + disableHelpHint + exitThisDialogHint; this._updateContextKeys(provider, true); this._getTextModel(URI.from({ path: `accessible-view-${provider.verbositySettingKey}`, scheme: 'accessible-view', fragment: this._currentContent })).then((model) => { @@ -402,7 +403,7 @@ export class AccessibleView extends Disposable { const verbose = this._configurationService.getValue(provider.verbositySettingKey); const hasActions = this._accessibleViewSupportsNavigation.get() || this._accessibleViewVerbosityEnabled.get() || this._accessibleViewGoToSymbolSupported.get() || this._currentProvider?.actions; if (verbose && !showAccessibleViewHelp && hasActions) { - actionsHint = localize('ariaAccessibleViewActions', "Use Shift+Tab to explore actions such as disabling this hint."); + actionsHint = localize('ariaAccessibleViewActions', 'Explore actions such as disabling this hint (Shift+Tab).'); } let ariaLabel = provider.options.type === AccessibleViewType.Help ? localize('accessibility-help', "Accessibility Help") : localize('accessible-view', "Accessible View"); this._title.textContent = ariaLabel; @@ -520,7 +521,7 @@ export class AccessibleView extends Disposable { private _getAccessibleViewHelpDialogContent(providerHasSymbols?: boolean): string { const navigationHint = this._getNavigationHint(); const goToSymbolHint = this._getGoToSymbolHint(providerHasSymbols); - const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab))"); + const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab))."); let hint = localize('intro', "In the accessible view, you can:\n"); if (navigationHint) { @@ -540,9 +541,9 @@ export class AccessibleView extends Disposable { const nextKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.ShowNext)?.getAriaLabel(); const previousKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.ShowPrevious)?.getAriaLabel(); if (nextKeybinding && previousKeybinding) { - hint = localize('accessibleViewNextPreviousHint', "Show the next ({0}) or previous ({1}) item", nextKeybinding, previousKeybinding); + hint = localize('accessibleViewNextPreviousHint', "Show the next ({0}) or previous ({1}) item.", nextKeybinding, previousKeybinding); } else { - hint = localize('chatAccessibleViewNextPreviousHintNoKb', "Show the next or previous item by configuring keybindings for the Show Next & Previous in Accessible View commands"); + hint = localize('chatAccessibleViewNextPreviousHintNoKb', "Show the next or previous item by configuring keybindings for the Show Next & Previous in Accessible View commands."); } return hint; } @@ -553,9 +554,9 @@ export class AccessibleView extends Disposable { let hint = ''; const disableKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.DisableVerbosityHint, this._contextKeyService)?.getAriaLabel(); if (disableKeybinding) { - hint = localize('acessibleViewDisableHint', "Disable accessibility verbosity for this feature ({0}). This will disable the hint to open the accessible view for example.\n", disableKeybinding); + hint = localize('acessibleViewDisableHint', "\n\nDisable accessibility verbosity for this feature ({0}).", disableKeybinding); } else { - hint = localize('accessibleViewDisableHintNoKb', "Add a keybinding for the command Disable Accessible View Hint, which disables accessibility verbosity for this feature.\n"); + hint = localize('accessibleViewDisableHintNoKb', "\n\nAdd a keybinding for the command Disable Accessible View Hint, which disables accessibility verbosity for this feature.s"); } return hint; } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 9a48e8ea78c..407239a8d86 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -62,8 +62,8 @@ import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/brows import { IChatCodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; +import { convertParsedRequestToMarkdown, walkTreeAndAnnotateResourceLinks } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { fixVariableReferences, walkTreeAndAnnotateResourceLinks } from 'vs/workbench/contrib/chat/browser/chatVariableReferenceRenderer'; import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IPlaceholderMarkdownString } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatReplyFollowup, IChatResponseProgressFileTreeData, IChatService, ISlashCommand, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; @@ -314,7 +314,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { diff --git a/src/vs/workbench/contrib/chat/browser/chatVariableReferenceRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts similarity index 60% rename from src/vs/workbench/contrib/chat/browser/chatVariableReferenceRenderer.ts rename to src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 1a7ad12b7b8..e6f7b341ae7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariableReferenceRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -4,27 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; +import { IParsedChatRequest, ChatRequestTextPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -const variableRefUrlPrefix = 'http://vscodeVar_'; +const variableRefUrl = 'http://_vscodeDecoration_'; -export function fixVariableReferences(markdown: IMarkdownString): IMarkdownString { - const fixedMarkdownSource = markdown.value.replace(/\]\(values:(.*)/g, `](${variableRefUrlPrefix}_$1`); - return new MarkdownString(fixedMarkdownSource, { isTrusted: markdown.isTrusted, supportThemeIcons: markdown.supportThemeIcons, supportHtml: markdown.supportHtml }); +export function convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string { + let result = ''; + for (const part of parsedRequest.parts) { + if (part instanceof ChatRequestTextPart) { + result += part.text; + } else { + result += `[${part.text}](${variableRefUrl})`; + } + } + + return result; } export function walkTreeAndAnnotateResourceLinks(element: HTMLElement): void { element.querySelectorAll('a').forEach(a => { const href = a.getAttribute('data-href'); if (href) { - if (href.startsWith(variableRefUrlPrefix)) { + if (href.startsWith(variableRefUrl)) { a.parentElement!.replaceChild( renderResourceWidget(a.textContent!), a); } } - - walkTreeAndAnnotateResourceLinks(a as HTMLElement); }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/chatQuick.ts index 5c41a1a6f7e..679b4ac99dc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuick.ts @@ -19,6 +19,7 @@ import { IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat import { IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; export class QuickChatService extends Disposable implements IQuickChatService { @@ -271,7 +272,7 @@ class QuickChat extends Disposable { for (const request of this.model.getRequests()) { if (request.response?.response.value || request.response?.errorDetails) { this.chatService.addCompleteRequest(widget.viewModel.sessionId, - request.message as string, + request.message as IParsedChatRequest, { message: request.response.response.value, errorDetails: request.response.errorDetails, diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 00e3979d1ff..6b1b18d87e8 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -26,7 +26,7 @@ import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart } from '../../common/chatRequestParser'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatService, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -127,7 +127,7 @@ class InputEditorDecorations extends Disposable { return; } - const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(viewModel.sessionId, inputValue); + const parsedRequest = (await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(viewModel.sessionId, inputValue)).parts; let placeholderDecoration: IDecorationOptions[] | undefined; const agentPart = parsedRequest.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); @@ -252,7 +252,7 @@ class SlashCommandCompletions extends Disposable { return null; } - const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()); + const parsedRequest = (await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue())).parts; const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); if (usedAgent) { // No (classic) global slash commands when an agent is used @@ -303,7 +303,7 @@ class AgentCompletions extends Disposable { return null; } - const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()); + const parsedRequest = (await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue())).parts; const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); if (usedAgent && !Range.containsPosition(usedAgent.editorRange, position)) { // Only one agent allowed @@ -340,7 +340,7 @@ class AgentCompletions extends Disposable { return; } - const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()); + const parsedRequest = (await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue())).parts; const usedAgent = parsedRequest.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); if (!usedAgent) { return; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 69ecd139299..092427c3dc0 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -10,7 +10,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { ILogService } from 'vs/platform/log/common/log'; -import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChat, IChatFollowup, IChatProgress, IChatReplyFollowup, IChatResponse, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; export interface IChatRequestModel { @@ -19,7 +20,7 @@ export interface IChatRequestModel { readonly username: string; readonly avatarIconUri?: URI; readonly session: IChatModel; - readonly message: string | IChatReplyFollowup; + readonly message: IParsedChatRequest | IChatReplyFollowup; readonly response: IChatResponseModel | undefined; } @@ -92,7 +93,7 @@ export class ChatRequestModel implements IChatRequestModel { constructor( public readonly session: ChatModel, - public readonly message: string | IChatReplyFollowup, + public readonly message: IParsedChatRequest | IChatReplyFollowup, private _providerRequestId?: string) { this._id = 'request_' + ChatRequestModel.nextId++; } @@ -483,8 +484,9 @@ export class ChatModel extends Disposable implements IChatModel { } get title(): string { - const firstRequestMessage = this._requests[0]?.message; - const message = typeof firstRequestMessage === 'string' ? firstRequestMessage : firstRequestMessage?.message ?? ''; + // const firstRequestMessage = this._requests[0]?.message; + // const message = typeof firstRequestMessage === 'string' ? firstRequestMessage : firstRequestMessage?.message ?? ''; + const message = ''; return message.split('\n')[0].substring(0, 50); } @@ -492,7 +494,6 @@ export class ChatModel extends Disposable implements IChatModel { public readonly providerId: string, private readonly initialData: ISerializableChatData | IExportableChatData | undefined, @ILogService private readonly logService: ILogService, - @IChatAgentService private readonly chatAgentService: IChatAgentService, ) { super(); @@ -518,14 +519,15 @@ export class ChatModel extends Disposable implements IChatModel { this._welcomeMessage = new ChatWelcomeMessageModel(this, content); } - return requests.map((raw: ISerializableChatRequestData) => { - const request = new ChatRequestModel(this, raw.message, raw.providerRequestId); - if (raw.response || raw.responseErrorDetails) { - const agent = raw.agent && this.chatAgentService.getAgents().find(a => a.id === raw.agent!.id); // TODO do something reasonable if this agent has disappeared since the last session - request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, true, raw.isCanceled, raw.vote, raw.providerRequestId, raw.responseErrorDetails, raw.followups); - } - return request; - }); + return []; + // return requests.map((raw: ISerializableChatRequestData) => { + // const request = new ChatRequestModel(this, raw.message, raw.providerRequestId); + // if (raw.response || raw.responseErrorDetails) { + // const agent = raw.agent && this.chatAgentService.getAgents().find(a => a.id === raw.agent!.id); // TODO do something reasonable if this agent has disappeared since the last session + // request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, true, raw.isCanceled, raw.vote, raw.providerRequestId, raw.responseErrorDetails, raw.followups); + // } + // return request; + // }); } startReinitialize(): void { @@ -569,7 +571,7 @@ export class ChatModel extends Disposable implements IChatModel { return this._requests; } - addRequest(message: string | IChatReplyFollowup, chatAgent?: IChatAgentData): ChatRequestModel { + addRequest(message: IParsedChatRequest | IChatReplyFollowup, chatAgent?: IChatAgentData): ChatRequestModel { if (!this._session) { throw new Error('addRequest: No session'); } @@ -677,7 +679,7 @@ export class ChatModel extends Disposable implements IChatModel { requests: this._requests.map((r): ISerializableChatRequestData => { return { providerRequestId: r.providerRequestId, - message: typeof r.message === 'string' ? r.message : r.message.message, + message: typeof r.message === 'string' ? r.message : '', response: r.response ? r.response.response.value : undefined, responseErrorDetails: r.response?.errorDetails, followups: r.response?.followups, diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts new file mode 100644 index 00000000000..14dbe0e24b6 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { IRange } from 'vs/editor/common/core/range'; +import { IChatAgentData, IChatAgentCommand } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; + +// These are in a separate file to avoid circular dependencies with the dependencies of the parser + +export interface IParsedChatRequest { + readonly parts: ReadonlyArray; + readonly text: string; +} + +export interface IParsedChatRequestPart { + readonly range: OffsetRange; + readonly editorRange: IRange; + readonly text: string; +} + +// TODO rename to tokens + +export class ChatRequestTextPart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string) { } +} + +/** + * An invocation of a static variable that can be resolved by the variable service + */ +export class ChatRequestVariablePart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly variableName: string, readonly variableArg: string) { } + + get text(): string { + const argPart = this.variableArg ? `:${this.variableArg}` : ''; + return `@${this.variableName}${argPart}`; + } +} + +/** + * An invocation of an agent that can be resolved by the agent service + */ +export class ChatRequestAgentPart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { } + + get text(): string { + return `@${this.agent.id}`; + } +} + +/** + * An invocation of an agent's subcommand + */ +export class ChatRequestAgentSubcommandPart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly command: IChatAgentCommand) { } + + get text(): string { + return `/${this.command.name}`; + } +} + +/** + * An invocation of a standalone slash command + */ +export class ChatRequestSlashCommandPart implements IParsedChatRequestPart { + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly slashCommand: ISlashCommand) { } + + get text(): string { + return `/${this.slashCommand.command}`; + } +} diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index b6ff47c6680..03bb2eb11f1 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -6,13 +6,14 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { IPosition, Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; -import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatService, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; +import { Range } from 'vs/editor/common/core/range'; +import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -const variableOrAgentReg = /^@([\w_\-]+)(:\d+)?(?=(\s|$))/i; // An @-variable with an optional numeric : arg (@response:2) -const slashReg = /\/([\w_-]+)(?=(\s|$))/i; // A / command +const variableOrAgentReg = /^@([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // An @-variable with an optional numeric : arg (@response:2) +const slashReg = /\/([\w_-]+)(?=(\s|$|\b))/i; // A / command export class ChatRequestParser { constructor( @@ -21,7 +22,7 @@ export class ChatRequestParser { @IChatService private readonly chatService: IChatService, ) { } - async parseChatRequest(sessionId: string, message: string): Promise { + async parseChatRequest(sessionId: string, message: string): Promise { const parts: IParsedChatRequestPart[] = []; let lineNumber = 1; @@ -68,7 +69,10 @@ export class ChatRequestParser { new Range(lastPart?.editorRange.endLineNumber ?? 1, lastPart?.editorRange.endColumn ?? 1, lineNumber, column), message.slice(lastPartEnd, message.length))); - return parts; + return { + parts, + text: message, + }; } private tryToParseVariableOrAgent(message: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestAgentPart | ChatRequestVariablePart | undefined { @@ -131,40 +135,3 @@ export class ChatRequestParser { return; } } - -export interface IParsedChatRequestPart { - readonly range: OffsetRange; - readonly editorRange: IRange; -} - -export class ChatRequestTextPart implements IParsedChatRequestPart { - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string) { } -} -/** - * An invocation of a static variable that can be resolved by the variable service - */ - -export class ChatRequestVariablePart implements IParsedChatRequestPart { - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly variableName: string, readonly variableArg: string) { } -} -/** - * An invocation of an agent that can be resolved by the agent service - */ - -export class ChatRequestAgentPart implements IParsedChatRequestPart { - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { } -} -/** - * An invocation of an agent's subcommand - */ - -export class ChatRequestAgentSubcommandPart implements IParsedChatRequestPart { - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly command: IChatAgentCommand) { } -} -/** - * An invocation of a standalone slash command - */ - -export class ChatRequestSlashCommandPart implements IParsedChatRequestPart { - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly slashCommand: ISlashCommand) { } -} diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 5c71168c493..f36f6e67854 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -12,6 +12,7 @@ import { IRange } from 'vs/editor/common/core/range'; import { ProviderResult } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChatModel, ChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChat { @@ -242,7 +243,7 @@ export interface IChatService { getSlashCommands(sessionId: string, token: CancellationToken): Promise; clearSession(sessionId: string): void; addRequest(context: any): void; - addCompleteRequest(sessionId: string, message: string, response: IChatCompleteResponse): void; + addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, response: IChatCompleteResponse): void; sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): void; getHistory(): IChatDetail[]; removeHistoryEntry(sessionId: string): void; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index f5fe279835f..d7f0f288eec 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -21,10 +21,12 @@ import { Progress } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatModel, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, ISerializableChatData, ISerializableChatsData, isCompleteInteractiveProgressTreeData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatRequestAgentPart, ChatRequestSlashCommandPart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider'; +import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatReplyFollowup, IChatRequest, IChatResponse, IChatService, IChatTransferredSessionData, IChatUserActionEvent, ISlashCommand, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService, IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -431,18 +433,21 @@ export class ChatService extends Disposable implements IChatService { } // This method is only returning whether the request was accepted - don't block on the actual request - return { responseCompletePromise: this._sendRequestAsync(model, provider, request, usedSlashCommand) }; + return { responseCompletePromise: this._sendRequestAsync(model, sessionId, provider, request, usedSlashCommand) }; } - private async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise { - const resolvedAgent = typeof message === 'string' ? this.resolveAgent(message) : undefined; - let request: ChatRequestModel; + private async _sendRequestAsync(model: ChatModel, sessionId: string, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise { + const parsedRequest = typeof message === 'string' ? + await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message) : + message; // Handle the followup type along with the response - const resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message; + let request: ChatRequestModel; + const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); + const commandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart); let gotProgress = false; const requestType = typeof message === 'string' ? - (message.startsWith('/') ? 'slashCommand' : 'string') : + commandPart ? 'slashCommand' : 'string' : 'followup'; const rawResponsePromise = createCancelablePromise(async token => { @@ -493,8 +498,8 @@ export class ChatService extends Disposable implements IChatService { let rawResponse: IChatResponse | null | undefined; let slashCommandFollowups: IChatFollowup[] | void = []; - if (typeof message === 'string' && resolvedAgent) { - request = model.addRequest(message); + if (typeof message === 'string' && agentPart) { + request = model.addRequest(parsedRequest); const history: IChatMessage[] = []; for (const request of model.getRequests()) { if (typeof request.message !== 'string' || !request.response) { @@ -505,15 +510,15 @@ export class ChatService extends Disposable implements IChatService { history.push({ role: ChatMessageRole.Assistant, content: request.response.response.value.value }); } } - const agentResult = await this.chatAgentService.invokeAgent(resolvedAgent.id, message.substring(resolvedAgent.id.length + 1).trimStart(), new Progress(p => { + const agentResult = await this.chatAgentService.invokeAgent(agentPart.agent.id, message.substring(agentPart.agent.id.length + 1).trimStart(), new Progress(p => { const { content } = p; const data = isCompleteInteractiveProgressTreeData(content) ? content : { content }; progressCallback(data); }), history, token); slashCommandFollowups = agentResult?.followUp; rawResponse = { session: model.session! }; - } else if ((typeof resolvedCommand === 'string' && typeof message === 'string' && this.chatSlashCommandService.hasCommand(resolvedCommand))) { - request = model.addRequest(message); + } else if (commandPart && typeof message === 'string' && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { + request = model.addRequest(parsedRequest); // contributed slash commands // TODO: spell this out in the UI const history: IChatMessage[] = []; @@ -526,7 +531,7 @@ export class ChatService extends Disposable implements IChatService { history.push({ role: ChatMessageRole.Assistant, content: request.response.response.value.value }); } } - const commandResult = await this.chatSlashCommandService.executeCommand(resolvedCommand, message.substring(resolvedCommand.length + 1).trimStart(), new Progress(p => { + const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress(p => { const { content } = p; const data = isCompleteInteractiveProgressTreeData(content) ? content : { content }; progressCallback(data); @@ -535,19 +540,18 @@ export class ChatService extends Disposable implements IChatService { rawResponse = { session: model.session! }; } else { + request = model.addRequest(parsedRequest); const requestProps: IChatRequest = { session: model.session!, - message: resolvedCommand, + message, variables: {} }; - if (typeof requestProps.message === 'string') { - const varResult = await this.chatVariablesService.resolveVariables(requestProps.message, model, token); + if ('parts' in parsedRequest) { + const varResult = await this.chatVariablesService.resolveVariables(parsedRequest, model, token); requestProps.variables = varResult.variables; requestProps.message = varResult.prompt; } - request = model.addRequest(requestProps.message); - rawResponse = await provider.provideReply(requestProps, progressCallback, token); } @@ -615,26 +619,6 @@ export class ChatService extends Disposable implements IChatService { provider.removeRequest?.(model.session!, requestId); } - private async handleSlashCommand(sessionId: string, command: string): Promise { - const slashCommands = await this.getSlashCommands(sessionId, CancellationToken.None); - for (const slashCommand of slashCommands ?? []) { - if (command.startsWith(`/${slashCommand.command}`) && this.chatSlashCommandService.hasCommand(slashCommand.command)) { - return slashCommand.command; - } - } - return command; - } - - private resolveAgent(prompt: string): IChatAgentData | undefined { - prompt = prompt.trim(); - const agents = this.chatAgentService.getAgents(); - if (!prompt.startsWith('@')) { - return; - } - - return agents.find(a => prompt.match(new RegExp(`@${a.id}($|\\s)`))); - } - async getSlashCommands(sessionId: string, token: CancellationToken): Promise { const model = this._sessionModels.get(sessionId); if (!model) { @@ -709,7 +693,7 @@ export class ChatService extends Disposable implements IChatService { return Array.from(this._providers.keys()); } - async addCompleteRequest(sessionId: string, message: string, response: IChatCompleteResponse): Promise { + async addCompleteRequest(sessionId: string, message: string | IParsedChatRequest, response: IChatCompleteResponse): Promise { this.trace('addCompleteRequest', `message: ${message}`); const model = this._sessionModels.get(sessionId); @@ -718,7 +702,8 @@ export class ChatService extends Disposable implements IChatService { } await model.waitForInitialization(); - const request = model.addRequest(message, undefined); + const parsedRequest = typeof message === 'string' ? await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message) : message; + const request = model.addRequest(parsedRequest); if (typeof response.message === 'string') { model.acceptResponseProgress(request, { content: response.message }); } else { diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 0fb80a2b0ef..baadbbed310 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -9,6 +9,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatRequestVariablePart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; export interface IChatVariableData { name: string; @@ -39,7 +40,7 @@ export interface IChatVariablesService { /** * Resolves all variables that occur in `prompt` */ - resolveVariables(prompt: string, model: IChatModel, token: CancellationToken): Promise; + resolveVariables(prompt: IParsedChatRequest, model: IChatModel, token: CancellationToken): Promise; } interface IChatData { @@ -60,40 +61,29 @@ export class ChatVariablesService implements IChatVariablesService { constructor() { } - async resolveVariables(prompt: string, model: IChatModel, token: CancellationToken): Promise { + async resolveVariables(prompt: IParsedChatRequest, model: IChatModel, token: CancellationToken): Promise { const resolvedVariables: Record = {}; const jobs: Promise[] = []; - // TODO have a separate parser that is also used for decorations - const regex = /(^|\s)@(\w+)(:\w+)?(?=\s|$|\b)/ig; - - let lastMatch = 0; const parsedPrompt: string[] = []; - let match: RegExpMatchArray | null; - while (match = regex.exec(prompt)) { - const [fullMatch, leading, varName, arg] = match; - const data = this._resolver.get(varName.toLowerCase()); - if (data) { - if (!arg || data.data.canTakeArgument) { - parsedPrompt.push(prompt.substring(lastMatch, match.index!)); - parsedPrompt.push(''); - lastMatch = match.index! + fullMatch.length; - const varIndex = parsedPrompt.length - 1; - const argWithoutColon = arg?.slice(1); - const fullVarName = varName + (arg ?? ''); - jobs.push(data.resolver(prompt, argWithoutColon, model, token).then(value => { - if (value) { - resolvedVariables[fullVarName] = value; - parsedPrompt[varIndex] = `${leading}[@${fullVarName}](values:${fullVarName})`; - } else { - parsedPrompt[varIndex] = fullMatch; - } - }).catch(onUnexpectedExternalError)); + prompt.parts + .forEach((varPart, i) => { + if (varPart instanceof ChatRequestVariablePart) { + const data = this._resolver.get(varPart.variableName.toLowerCase()); + if (data) { + jobs.push(data.resolver(prompt.text, varPart.variableArg, model, token).then(value => { + if (value) { + resolvedVariables[varPart.variableName] = value; + parsedPrompt[i] = `[@${varPart.variableName}](values:${varPart.variableName})`; + } else { + parsedPrompt[i] = varPart.text; + } + }).catch(onUnexpectedExternalError)); + } + } else { + parsedPrompt[i] = varPart.text; } - } - } - - parsedPrompt.push(prompt.substring(lastMatch)); + }); await Promise.allSettled(jobs); diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 7e891ea7be6..2238e4d949f 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -11,6 +11,7 @@ import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IChatModel, IChatRequestModel, IChatResponseModel, IChatWelcomeMessageContent, IResponse, Response } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatReplyFollowup, IChatResponseCommandFollowup, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; @@ -51,7 +52,7 @@ export interface IChatRequestViewModel { readonly dataId: string; readonly username: string; readonly avatarIconUri?: URI; - readonly message: string | IChatReplyFollowup; + readonly message: IParsedChatRequest | IChatReplyFollowup; readonly messageText: string; currentRenderedHeight: number | undefined; } @@ -215,7 +216,7 @@ export class ChatRequestViewModel implements IChatRequestViewModel { } get messageText() { - return typeof this.message === 'string' ? this.message : this.message.message; + return 'kind' in this.message ? this.message.message : this.message.text; } currentRenderedHeight: number | undefined; diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__agents_and_variables_and_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__agents_and_variables_and_multiline.0.snap deleted file mode 100644 index ca127ef833b..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__agents_and_variables_and_multiline.0.snap +++ /dev/null @@ -1,60 +0,0 @@ -[ - { - range: { - start: 0, - endExclusive: 6 - }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 7 - }, - agent: { - id: "agent", - metadata: { - description: "", - subCommands: [ { name: "subCommand" } ] - } - } - }, - { - range: { - start: 6, - endExclusive: 18 - }, - editorRange: { - startLineNumber: 1, - startColumn: 7, - endLineNumber: 2, - endColumn: 4 - }, - text: " Please \ndo " - }, - { - range: { - start: 18, - endExclusive: 29 - }, - editorRange: { - startLineNumber: 2, - startColumn: 4, - endLineNumber: 2, - endColumn: 15 - }, - command: { name: "subCommand" } - }, - { - range: { - start: 29, - endExclusive: 63 - }, - editorRange: { - startLineNumber: 2, - startColumn: 15, - endLineNumber: 3, - endColumn: 18 - }, - text: " with @selection\nand @debugConsole" - } -] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__plain_text_with_newlines.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__plain_text_with_newlines.0.snap deleted file mode 100644 index 31b7d3be458..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser__plain_text_with_newlines.0.snap +++ /dev/null @@ -1,15 +0,0 @@ -[ - { - range: { - start: 0, - endExclusive: 21 - }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 3, - endColumn: 7 - }, - text: "line 1\nline 2\r\nline 3" - } -] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_not_first.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_not_first.0.snap index b7b48f33be9..3a65b0a6ea4 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_not_first.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_not_first.0.snap @@ -1,73 +1,76 @@ -[ - { - range: { - start: 0, - endExclusive: 10 +{ + parts: [ + { + range: { + start: 0, + endExclusive: 10 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 11 + }, + text: "Hello Mr. " }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 11 - }, - text: "Hello Mr. " - }, - { - range: { - start: 10, - endExclusive: 16 - }, - editorRange: { - startLineNumber: 1, - startColumn: 11, - endLineNumber: 1, - endColumn: 17 - }, - agent: { - id: "agent", - metadata: { - description: "", - subCommands: [ { name: "subCommand" } ] + { + range: { + start: 10, + endExclusive: 16 + }, + editorRange: { + startLineNumber: 1, + startColumn: 11, + endLineNumber: 1, + endColumn: 17 + }, + agent: { + id: "agent", + metadata: { + description: "", + subCommands: [ { name: "subCommand" } ] + } } + }, + { + range: { + start: 16, + endExclusive: 17 + }, + editorRange: { + startLineNumber: 1, + startColumn: 17, + endLineNumber: 1, + endColumn: 18 + }, + text: " " + }, + { + range: { + start: 17, + endExclusive: 28 + }, + editorRange: { + startLineNumber: 1, + startColumn: 18, + endLineNumber: 1, + endColumn: 29 + }, + command: { name: "subCommand" } + }, + { + range: { + start: 28, + endExclusive: 35 + }, + editorRange: { + startLineNumber: 1, + startColumn: 29, + endLineNumber: 1, + endColumn: 36 + }, + text: " thanks" } - }, - { - range: { - start: 16, - endExclusive: 17 - }, - editorRange: { - startLineNumber: 1, - startColumn: 17, - endLineNumber: 1, - endColumn: 18 - }, - text: " " - }, - { - range: { - start: 17, - endExclusive: 28 - }, - editorRange: { - startLineNumber: 1, - startColumn: 18, - endLineNumber: 1, - endColumn: 29 - }, - command: { name: "subCommand" } - }, - { - range: { - start: 28, - endExclusive: 35 - }, - editorRange: { - startLineNumber: 1, - startColumn: 29, - endLineNumber: 1, - endColumn: 36 - }, - text: " thanks" - } -] \ No newline at end of file + ], + text: "Hello Mr. @agent /subCommand thanks" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap new file mode 100644 index 00000000000..ce4e5ebadba --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap @@ -0,0 +1,50 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 14 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 15 + }, + text: "Are you there " + }, + { + range: { + start: 14, + endExclusive: 20 + }, + editorRange: { + startLineNumber: 1, + startColumn: 15, + endLineNumber: 1, + endColumn: 21 + }, + agent: { + id: "agent", + metadata: { + description: "", + subCommands: [ { name: "subCommand" } ] + } + } + }, + { + range: { + start: 20, + endExclusive: 21 + }, + editorRange: { + startLineNumber: 1, + startColumn: 21, + endLineNumber: 1, + endColumn: 22 + }, + text: "?" + } + ], + text: "Are you there @agent?" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents.0.snap index 85afe6b0ae1..9b5a1010a4c 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents.0.snap @@ -1,60 +1,63 @@ -[ - { - range: { - start: 0, - endExclusive: 6 - }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 7 - }, - agent: { - id: "agent", - metadata: { - description: "", - subCommands: [ { name: "subCommand" } ] +{ + parts: [ + { + range: { + start: 0, + endExclusive: 6 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 7 + }, + agent: { + id: "agent", + metadata: { + description: "", + subCommands: [ { name: "subCommand" } ] + } } + }, + { + range: { + start: 6, + endExclusive: 17 + }, + editorRange: { + startLineNumber: 1, + startColumn: 7, + endLineNumber: 1, + endColumn: 18 + }, + text: " Please do " + }, + { + range: { + start: 17, + endExclusive: 28 + }, + editorRange: { + startLineNumber: 1, + startColumn: 18, + endLineNumber: 1, + endColumn: 29 + }, + command: { name: "subCommand" } + }, + { + range: { + start: 28, + endExclusive: 35 + }, + editorRange: { + startLineNumber: 1, + startColumn: 29, + endLineNumber: 1, + endColumn: 36 + }, + text: " thanks" } - }, - { - range: { - start: 6, - endExclusive: 17 - }, - editorRange: { - startLineNumber: 1, - startColumn: 7, - endLineNumber: 1, - endColumn: 18 - }, - text: " Please do " - }, - { - range: { - start: 17, - endExclusive: 28 - }, - editorRange: { - startLineNumber: 1, - startColumn: 18, - endLineNumber: 1, - endColumn: 29 - }, - command: { name: "subCommand" } - }, - { - range: { - start: 28, - endExclusive: 35 - }, - editorRange: { - startLineNumber: 1, - startColumn: 29, - endLineNumber: 1, - endColumn: 36 - }, - text: " thanks" - } -] \ No newline at end of file + ], + text: "@agent Please do /subCommand thanks" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap new file mode 100644 index 00000000000..d74138f7fb4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap @@ -0,0 +1,63 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 6 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 7 + }, + agent: { + id: "agent", + metadata: { + description: "", + subCommands: [ { name: "subCommand" } ] + } + } + }, + { + range: { + start: 6, + endExclusive: 18 + }, + editorRange: { + startLineNumber: 1, + startColumn: 7, + endLineNumber: 2, + endColumn: 4 + }, + text: " Please \ndo " + }, + { + range: { + start: 18, + endExclusive: 29 + }, + editorRange: { + startLineNumber: 2, + startColumn: 4, + endLineNumber: 2, + endColumn: 15 + }, + command: { name: "subCommand" } + }, + { + range: { + start: 29, + endExclusive: 63 + }, + editorRange: { + startLineNumber: 2, + startColumn: 15, + endLineNumber: 3, + endColumn: 18 + }, + text: " with @selection\nand @debugConsole" + } + ], + text: "@agent Please \ndo /subCommand with @selection\nand @debugConsole" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_slash_command.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_slash_command.0.snap index e1389633f29..86e07d1cb2a 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_slash_command.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_slash_command.0.snap @@ -1,15 +1,18 @@ -[ - { - range: { - start: 0, - endExclusive: 13 - }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 14 - }, - text: "/explain this" - } -] \ No newline at end of file +{ + parts: [ + { + range: { + start: 0, + endExclusive: 13 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 14 + }, + text: "/explain this" + } + ], + text: "/explain this" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_variables.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_variables.0.snap index d3fa8a51d2d..854189c56e9 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_variables.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_variables.0.snap @@ -1,15 +1,18 @@ -[ - { - range: { - start: 0, - endExclusive: 26 - }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 27 - }, - text: "What does @selection mean?" - } -] \ No newline at end of file +{ + parts: [ + { + range: { + start: 0, + endExclusive: 26 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 27 + }, + text: "What does @selection mean?" + } + ], + text: "What does @selection mean?" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_multiple_slash_commands.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_multiple_slash_commands.0.snap index 3e1f3c0147e..9babacf74e1 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_multiple_slash_commands.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_multiple_slash_commands.0.snap @@ -1,28 +1,31 @@ -[ - { - range: { - start: 0, - endExclusive: 4 +{ + parts: [ + { + range: { + start: 0, + endExclusive: 4 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 5 + }, + slashCommand: { command: "fix" } }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 5 - }, - slashCommand: { command: "fix" } - }, - { - range: { - start: 4, - endExclusive: 9 - }, - editorRange: { - startLineNumber: 1, - startColumn: 5, - endLineNumber: 1, - endColumn: 10 - }, - text: " /fix" - } -] \ No newline at end of file + { + range: { + start: 4, + endExclusive: 9 + }, + editorRange: { + startLineNumber: 1, + startColumn: 5, + endLineNumber: 1, + endColumn: 10 + }, + text: " /fix" + } + ], + text: "/fix /fix" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text.0.snap index d032da60253..e5e1fac6b73 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text.0.snap @@ -1,15 +1,18 @@ -[ - { - range: { - start: 0, - endExclusive: 4 - }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 5 - }, - text: "test" - } -] \ No newline at end of file +{ + parts: [ + { + range: { + start: 0, + endExclusive: 4 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 5 + }, + text: "test" + } + ], + text: "test" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text_with_newlines.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text_with_newlines.0.snap new file mode 100644 index 00000000000..7f0c88fb724 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text_with_newlines.0.snap @@ -0,0 +1,18 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 21 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 3, + endColumn: 7 + }, + text: "line 1\nline 2\r\nline 3" + } + ], + text: "line 1\nline 2\r\nline 3" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command.0.snap index a2dadb07783..75e6df87612 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command.0.snap @@ -1,28 +1,31 @@ -[ - { - range: { - start: 0, - endExclusive: 4 +{ + parts: [ + { + range: { + start: 0, + endExclusive: 4 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 5 + }, + slashCommand: { command: "fix" } }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 5 - }, - slashCommand: { command: "fix" } - }, - { - range: { - start: 4, - endExclusive: 9 - }, - editorRange: { - startLineNumber: 1, - startColumn: 5, - endLineNumber: 1, - endColumn: 10 - }, - text: " this" - } -] \ No newline at end of file + { + range: { + start: 4, + endExclusive: 9 + }, + editorRange: { + startLineNumber: 1, + startColumn: 5, + endLineNumber: 1, + endColumn: 10 + }, + text: " this" + } + ], + text: "/fix this" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap new file mode 100644 index 00000000000..a6b846cf943 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap @@ -0,0 +1,45 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 8 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 9 + }, + text: "What is " + }, + { + range: { + start: 8, + endExclusive: 18 + }, + editorRange: { + startLineNumber: 1, + startColumn: 9, + endLineNumber: 1, + endColumn: 19 + }, + variableName: "selection", + variableArg: "" + }, + { + range: { + start: 18, + endExclusive: 19 + }, + editorRange: { + startLineNumber: 1, + startColumn: 19, + endLineNumber: 1, + endColumn: 20 + }, + text: "?" + } + ], + text: "What is @selection?" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap index 75fe064e9aa..721dbc46117 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap @@ -1,42 +1,45 @@ -[ - { - range: { - start: 0, - endExclusive: 10 +{ + parts: [ + { + range: { + start: 0, + endExclusive: 10 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 11 + }, + text: "What does " }, - editorRange: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 11 + { + range: { + start: 10, + endExclusive: 20 + }, + editorRange: { + startLineNumber: 1, + startColumn: 11, + endLineNumber: 1, + endColumn: 21 + }, + variableName: "selection", + variableArg: "" }, - text: "What does " - }, - { - range: { - start: 10, - endExclusive: 20 - }, - editorRange: { - startLineNumber: 1, - startColumn: 11, - endLineNumber: 1, - endColumn: 21 - }, - variableName: "selection", - variableArg: "" - }, - { - range: { - start: 20, - endExclusive: 26 - }, - editorRange: { - startLineNumber: 1, - startColumn: 21, - endLineNumber: 1, - endColumn: 27 - }, - text: " mean?" - } -] \ No newline at end of file + { + range: { + start: 20, + endExclusive: 26 + }, + editorRange: { + startLineNumber: 1, + startColumn: 21, + endLineNumber: 1, + endColumn: 27 + }, + text: " mean?" + } + ], + text: "What does @selection mean?" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts index 7d207a080e5..221d59a4de4 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -36,7 +36,7 @@ suite('ChatRequestParser', () => { await assertSnapshot(result); }); - test('_plain text with newlines', async () => { + test('plain text with newlines', async () => { parser = instantiationService.createInstance(ChatRequestParser); const text = 'line 1\nline 2\r\nline 3'; const result = await parser.parseChatRequest('1', text); @@ -87,6 +87,17 @@ suite('ChatRequestParser', () => { await assertSnapshot(result); }); + test('variable with question mark', async () => { + const variablesService = mockObject()({}); + variablesService.hasVariable.returns(true); + instantiationService.stub(IChatVariablesService, variablesService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = 'What is @selection?'; + const result = await parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + test('invalid variables', async () => { const variablesService = mockObject()({}); variablesService.hasVariable.returns(false); @@ -108,6 +119,16 @@ suite('ChatRequestParser', () => { await assertSnapshot(result); }); + test('agent with question mark', async () => { + const agentsService = mockObject()({}); + agentsService.getAgent.returns({ id: 'agent', metadata: { description: '', subCommands: [{ name: 'subCommand' }] } }); + instantiationService.stub(IChatAgentService, agentsService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const result = await parser.parseChatRequest('1', 'Are you there @agent?'); + await assertSnapshot(result); + }); + test('agent not first', async () => { const agentsService = mockObject()({}); agentsService.getAgent.returns({ id: 'agent', metadata: { description: '', subCommands: [{ name: 'subCommand' }] } }); @@ -118,7 +139,7 @@ suite('ChatRequestParser', () => { await assertSnapshot(result); }); - test('_agents and variables and multiline', async () => { + test('agents and variables and multiline', async () => { const agentsService = mockObject()({}); agentsService.getAgent.returns({ id: 'agent', metadata: { description: '', subCommands: [{ name: 'subCommand' }] } }); instantiationService.stub(IChatAgentService, agentsService as any); 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 b3ccd1a70a6..9cf2a92c988 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -94,11 +94,11 @@ suite('Chat', () => { const session1 = testDisposables.add(testService.startSession('provider1', CancellationToken.None)); await session1.waitForInitialization(); - session1!.addRequest('request 1'); + session1!.addRequest({ parts: [], text: 'request 1' }); const session2 = testDisposables.add(testService.startSession('provider2', CancellationToken.None)); await session2.waitForInitialization(); - session2!.addRequest('request 2'); + session2!.addRequest({ parts: [], text: 'request 2' }); assert.strictEqual(provider1.lastInitialState, undefined); assert.strictEqual(provider2.lastInitialState, undefined); diff --git a/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts b/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts index f67df48598e..dae6a364994 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts @@ -6,58 +6,78 @@ import * as assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { ChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; +import { ChatVariablesService, IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; suite('ChatVariables', function () { let service: ChatVariablesService; + let instantiationService: TestInstantiationService; + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); setup(function () { service = new ChatVariablesService(); + instantiationService = testDisposables.add(new TestInstantiationService()); + instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IExtensionService, new TestExtensionService()); + instantiationService.stub(IChatVariablesService, service); + instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); }); - ensureNoDisposablesAreLeakedInTestSuite(); - test('ChatVariables - resolveVariables', async function () { const v1 = service.registerVariable({ name: 'foo', description: 'bar' }, async () => ([{ level: 'full', value: 'farboo' }])); const v2 = service.registerVariable({ name: 'far', description: 'boo' }, async () => ([{ level: 'full', value: 'farboo' }])); + const parser = instantiationService.createInstance(ChatRequestParser); + + const resolveVariables = async (text: string) => { + const result = await parser.parseChatRequest('1', text); + return await service.resolveVariables(result, null!, CancellationToken.None); + }; + { - const data = await service.resolveVariables('Hello @foo and@far', null!, CancellationToken.None); + const data = await resolveVariables('Hello @foo and@far'); assert.strictEqual(Object.keys(data.variables).length, 1); assert.deepEqual(Object.keys(data.variables).sort(), ['foo']); assert.strictEqual(data.prompt, 'Hello [@foo](values:foo) and@far'); } { - const data = await service.resolveVariables('@foo Hello', null!, CancellationToken.None); + const data = await resolveVariables('@foo Hello'); assert.strictEqual(Object.keys(data.variables).length, 1); assert.deepEqual(Object.keys(data.variables).sort(), ['foo']); assert.strictEqual(data.prompt, '[@foo](values:foo) Hello'); } { - const data = await service.resolveVariables('Hello @foo', null!, CancellationToken.None); + const data = await resolveVariables('Hello @foo'); assert.strictEqual(Object.keys(data.variables).length, 1); assert.deepEqual(Object.keys(data.variables).sort(), ['foo']); } { - const data = await service.resolveVariables('Hello @foo?', null!, CancellationToken.None); + const data = await resolveVariables('Hello @foo?'); assert.strictEqual(Object.keys(data.variables).length, 1); assert.deepEqual(Object.keys(data.variables).sort(), ['foo']); assert.strictEqual(data.prompt, 'Hello [@foo](values:foo)?'); } { - const data = await service.resolveVariables('Hello @foo and@far @foo', null!, CancellationToken.None); + const data = await resolveVariables('Hello @foo and@far @foo'); assert.strictEqual(Object.keys(data.variables).length, 1); assert.deepEqual(Object.keys(data.variables).sort(), ['foo']); } { - const data = await service.resolveVariables('Hello @foo and @far @foo', null!, CancellationToken.None); + const data = await resolveVariables('Hello @foo and @far @foo'); assert.strictEqual(Object.keys(data.variables).length, 2); assert.deepEqual(Object.keys(data.variables).sort(), ['far', 'foo']); } { - const data = await service.resolveVariables('Hello @foo and @far @foo @unknown', null!, CancellationToken.None); + const data = await resolveVariables('Hello @foo and @far @foo @unknown'); assert.strictEqual(Object.keys(data.variables).length, 2); assert.deepEqual(Object.keys(data.variables).sort(), ['far', 'foo']); assert.strictEqual(data.prompt, 'Hello [@foo](values:foo) and [@far](values:far) [@foo](values:foo) @unknown'); diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index a7e9ee09995..d447e6d5355 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -68,19 +68,21 @@ registerSingleton(ICommentService, CommentService, InstantiationType.Delayed); export namespace CommentAccessibilityHelpNLS { - export const escape = nls.localize('escape', "Dismiss the comment widget via Escape."); - export const nextRange = nls.localize('next', "Navigate to the next commenting range via ({0})."); - export const nextRangeNoKb = nls.localize('nextNoKb', "Run the command: Go to Next Commenting Range, which is currently not triggerable via keybinding."); - export const previousRange = nls.localize('previous', "Navigate to the previous comment range via ({0})."); + export const intro = nls.localize('intro', "The editor contains a commentable range. Some useful commands include:"); + export const introWidget = nls.localize('introWidget', "Some useful comment commands include:"); + export const escape = nls.localize('escape', "- Dismiss Comment (Escape)"); + export const nextRange = nls.localize('next', "- Navigate to the next commenting range ({0})"); + export const nextRangeNoKb = nls.localize('nextNoKb', "- Go to Next Commenting Range, which is currently not triggerable via keybinding."); + export const previousRange = nls.localize('previous', "- Navigate to the previous commenting range ({0})"); export const previousRangeNoKb = nls.localize('previousNoKb', "Run the command: Go to Previous Commenting Range, which is currently not triggerable via keybinding."); - export const nextCommentThreadKb = nls.localize('nextCommentThreadKb', "Navigate to the next comment thread via ({0})."); - export const nextCommentThreadNoKb = nls.localize('nextCommentThreadNoKb', "Run the command: Go to Next Comment Thread, which is currently not triggerable via keybinding."); - export const previousCommentThreadKb = nls.localize('previousCommentThreadKb', "Navigate to the previous comment thread via ({0})."); - export const previousCommentThreadNoKb = nls.localize('previousCommentThreadNoKb', "Run the command: Go to Previous Comment Thread, which is currently not triggerable via keybinding."); - export const addComment = nls.localize('addComment', "Add a comment via ({0})."); - export const addCommentNoKb = nls.localize('addCommentNoKb', "Add a comment via the command: Add Comment on Current Selection, which is currently not triggerable via keybinding."); - export const submitComment = nls.localize('submitComment', "Submit the comment via ({0})."); - export const submitCommentNoKb = nls.localize('submitCommentNoKb', "Submit the comment by navigating with tab to the button, as it's currently not triggerable via keybinding."); + export const nextCommentThreadKb = nls.localize('nextCommentThreadKb', "- Navigate to the next comment thread ({0})"); + export const nextCommentThreadNoKb = nls.localize('nextCommentThreadNoKb', "- Run the command: Go to Next Comment Thread, which is currently not triggerable via keybinding."); + export const previousCommentThreadKb = nls.localize('previousCommentThreadKb', "- Navigate to the previous comment thread ({0})"); + export const previousCommentThreadNoKb = nls.localize('previousCommentThreadNoKb', "- Run the command: Go to Previous Comment Thread, which is currently not triggerable via keybinding."); + export const addComment = nls.localize('addComment', "- Add Comment ({0})"); + export const addCommentNoKb = nls.localize('addCommentNoKb', "- Add Comment on Current Selection, which is currently not triggerable via keybinding."); + export const submitComment = nls.localize('submitComment', "- Submit Comment ({0})"); + export const submitCommentNoKb = nls.localize('submitCommentNoKb', "- Submit Comment, accessible via tabbing, as it's currently not triggerable with a keybinding."); } export class CommentsAccessibilityHelpContribution extends Disposable { @@ -114,12 +116,13 @@ export class CommentsAccessibilityHelpProvider implements IAccessibleContentProv provideContent(): string { this._element = document.activeElement as HTMLElement; const content: string[] = []; + content.push(CommentAccessibilityHelpNLS.introWidget); content.push(CommentAccessibilityHelpNLS.escape); content.push(this._descriptionForCommand(CommentCommandId.Add, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.addCommentNoKb)); + content.push(this._descriptionForCommand(CommentCommandId.Submit, CommentAccessibilityHelpNLS.submitComment, CommentAccessibilityHelpNLS.submitCommentNoKb)); content.push(this._descriptionForCommand(CommentCommandId.NextRange, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.nextRangeNoKb)); content.push(this._descriptionForCommand(CommentCommandId.PreviousRange, CommentAccessibilityHelpNLS.previousRange, CommentAccessibilityHelpNLS.previousRangeNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.Submit, CommentAccessibilityHelpNLS.submitComment, CommentAccessibilityHelpNLS.submitCommentNoKb)); - return content.join('\n\n'); + return content.join('\n'); } onClose(): void { this._element?.focus(); diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 5e42b720516..412aa072ba2 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -851,7 +851,6 @@ export class CommentController implements IEditorContribution { if (!this._hasRespondedToEditorChange) { if (this._commentInfos.some(commentInfo => commentInfo.commentingRanges.ranges.length > 0 || commentInfo.commentingRanges.fileComments)) { this._hasRespondedToEditorChange = true; - this._activeEditorHasCommentingRange.set(true); const verbose = this.configurationService.getValue(AccessibilityVerbositySettingId.Comments); if (verbose) { const keybinding = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getAriaLabel(); @@ -863,8 +862,6 @@ export class CommentController implements IEditorContribution { } else { status(nls.localize('hasCommentRanges', "Editor has commenting ranges.")); } - } else { - this._activeEditorHasCommentingRange.set(false); } } }); @@ -1129,7 +1126,12 @@ export class CommentController implements IEditorContribution { // create viewzones this.removeCommentWidgetsAndStoreCache(); + let hasCommentingRanges = false; this._commentInfos.forEach(info => { + if (!hasCommentingRanges && (info.commentingRanges.ranges.length > 0 || info.commentingRanges.fileComments)) { + hasCommentingRanges = true; + } + const providerCacheStore = this._pendingNewCommentCache[info.owner]; const providerEditsCacheStore = this._pendingEditsCache[info.owner]; info.threads = info.threads.filter(thread => !thread.isDisposed); @@ -1157,6 +1159,12 @@ export class CommentController implements IEditorContribution { this._commentingRangeDecorator.update(this.editor, this._commentInfos); this._commentThreadRangeDecorator.update(this.editor, this._commentInfos); + + if (hasCommentingRanges) { + this._activeEditorHasCommentingRange.set(true); + } else { + this._activeEditorHasCommentingRange.set(false); + } } public closeWidget(): void { diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index be0a96f0d63..9889c3f7fab 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -85,6 +85,15 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ weight: KeybindingWeight.EditorContrib }); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: CommentCommandId.NextRange, + title: nls.localize('comments.nextCommentingRange', "Go to Next Commenting Range"), + category: 'Comments', + }, + when: CommentContextKeys.activeEditorHasCommentingRange +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CommentCommandId.PreviousRange, handler: async (accessor, args?: { range: IRange; fileComment: boolean }) => { @@ -104,6 +113,15 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ weight: KeybindingWeight.EditorContrib }); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: CommentCommandId.PreviousRange, + title: nls.localize('comments.previousCommentingRange', "Go to Previous Commenting Range"), + category: 'Comments', + }, + when: CommentContextKeys.activeEditorHasCommentingRange +}); + CommandsRegistry.registerCommand({ id: CommentCommandId.ToggleCommenting, handler: (accessor) => { diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts index b93540ddfaf..e62b646de33 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { IReference } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ICustomEditorModel, ICustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditor'; @@ -45,7 +45,7 @@ export class CustomEditorModelManager implements ICustomEditorModelManager { return entry.model.then(model => { return { object: model, - dispose: once(() => { + dispose: createSingleCallFunction(() => { if (--entry!.counter <= 0) { entry.model.then(x => x.dispose()); this._references.delete(key); diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index ae28c17ad96..a3e99830ac3 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -12,7 +12,7 @@ import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; @@ -214,7 +214,7 @@ export abstract class AbstractExpressionsRenderer implements IT inputBox.focus(); inputBox.select(); - const done = once((success: boolean, finishEditing: boolean) => { + const done = createSingleCallFunction((success: boolean, finishEditing: boolean) => { nameElement.style.display = ''; valueElement.style.display = ''; inputBoxContainer.style.display = 'none'; diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 5e56cb350b2..141f0d1ab2f 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -417,6 +417,8 @@ class WatchExpressionsDragAndDrop implements ITreeDragAndDrop { const position = targetElement instanceof Expression ? watches.indexOf(targetElement) : watches.length - 1; this.debugService.moveWatchExpression(draggedElement.getId(), position); } + + dispose(): void { } } registerAction2(class Collapse extends ViewAction { diff --git a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index b886e417338..c77b79f07cc 100644 --- a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -128,7 +128,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { } // re-schedule this bit of the operation to be off the critical path - in case glob-match is slow - this._register(disposableTimeout(() => this.promptImportantRecommendations(uri, model), 0)); + disposableTimeout(() => this.promptImportantRecommendations(uri, model), 0, this._store); } /** @@ -232,12 +232,12 @@ export class FileBasedRecommendations extends ExtensionRecommendations { const disposables = new DisposableStore(); disposables.add(model.onDidChangeLanguage(() => { // re-schedule this bit of the operation to be off the critical path - in case glob-match is slow - disposables.add(disposableTimeout(() => { + disposableTimeout(() => { if (!disposables.isDisposed) { this.promptImportantRecommendations(uri, model, unmatchedRecommendations); disposables.dispose(); } - }, 0)); + }, 0, disposables); })); disposables.add(model.onWillDispose(() => disposables.dispose())); } diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index 56c7149a0ae..98947a52841 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -29,7 +29,7 @@ import { FileAccess, Schemas } from 'vs/base/common/network'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { listenStream } from 'vs/base/common/stream'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { coalesce } from 'vs/base/common/arrays'; import { canceled } from 'vs/base/common/errors'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -710,7 +710,7 @@ export class FileDownload { const disposables = new DisposableStore(); disposables.add(toDisposable(() => target.close())); - disposables.add(once(token.onCancellationRequested)(() => { + disposables.add(createSingleCallFunction(token.onCancellationRequested)(() => { disposables.dispose(); reject(canceled()); })); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index a25c052188a..bfebab5f73d 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -23,7 +23,7 @@ import { IFilesConfiguration, UndoConfirmLevel } from 'vs/workbench/contrib/file import { dirname, joinPath, distinctParents } from 'vs/base/common/resources'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { localize } from 'vs/nls'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'vs/base/common/path'; @@ -559,7 +559,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer 0 && !stat.isDirectory ? lastDot : value.length }); - const done = once((success: boolean, finishEditing: boolean) => { + const done = createSingleCallFunction((success: boolean, finishEditing: boolean) => { label.element.style.display = 'none'; const value = inputBox.value; dispose(toDispose); @@ -1039,7 +1039,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { private compressedDragOverElement: HTMLElement | undefined; private compressedDropTargetDisposable: IDisposable = Disposable.None; - private toDispose: IDisposable[]; + private disposables = new DisposableStore(); private dropEnabled = false; constructor( @@ -1054,15 +1054,13 @@ export class FileDragAndDrop implements ITreeDragAndDrop { @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { - this.toDispose = []; - const updateDropEnablement = (e: IConfigurationChangeEvent | undefined) => { if (!e || e.affectsConfiguration('explorer.enableDragAndDrop')) { this.dropEnabled = this.configurationService.getValue('explorer.enableDragAndDrop'); } }; updateDropEnablement(undefined); - this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => updateDropEnablement(e))); + this.disposables.add(this.configurationService.onDidChangeConfiguration(e => updateDropEnablement(e))); } onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction { @@ -1482,6 +1480,10 @@ export class FileDragAndDrop implements ITreeDragAndDrop { onDragEnd(): void { this.compressedDropTargetDisposable.dispose(); } + + dispose(): void { + this.compressedDropTargetDisposable.dispose(); + } } function getIconLabelNameFromHTMLElement(target: HTMLElement | EventTarget | Element | null): { element: HTMLElement; count: number; index: number } | null { diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 593b813f336..ee80e9d0858 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -711,6 +711,8 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop group, () => group.focus(), { index }); } } + + dispose(): void { } } class OpenEditorsAccessibilityProvider implements IListAccessibilityProvider { diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index c525fb6bb73..edf32a0e189 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -17,7 +17,7 @@ import { ILanguageService, ILanguageSelection } from 'vs/editor/common/languages import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { once } from 'vs/base/common/functional'; +import { Event } from 'vs/base/common/event'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { localize } from 'vs/nls'; @@ -198,7 +198,7 @@ export class TextFileContentProvider extends Disposable implements ITextModelCon })); if (codeEditorModel) { - disposables.add(once(codeEditorModel.onWillDispose)(() => this.fileWatcherDisposable.clear())); + disposables.add(Event.once(codeEditorModel.onWillDispose)(() => this.fileWatcherDisposable.clear())); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 96f58a5b6f8..046890e899f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -7,20 +7,15 @@ import { registerAction2 } from 'vs/platform/actions/common/actions'; import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import * as InlineChatActions from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, IInlineChatService, INLINE_CHAT_ID, INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { IInlineChatService, INLINE_CHAT_ID, INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; import { IInlineChatSessionService, InlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { InlineChatNotebookContribution } from 'vs/workbench/contrib/inlineChat/browser/inlineChatNotebook'; -import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { Extensions, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { InlineChatAccessibleViewContribution } from './inlineChatAccessibleView'; registerSingleton(IInlineChatService, InlineChatServiceImpl, InstantiationType.Delayed); registerSingleton(IInlineChatSessionService, InlineChatSessionService, InstantiationType.Delayed); @@ -54,44 +49,6 @@ registerAction2(InlineChatActions.ApplyPreviewEdits); registerAction2(InlineChatActions.CopyRecordings); - -Registry.as(Extensions.Workbench) - .registerWorkbenchContribution(InlineChatNotebookContribution, LifecyclePhase.Restored); - - -class InlineChatAccessibleViewContribution extends Disposable { - static ID: 'inlineChatAccessibleViewContribution'; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(100, 'inlineChat', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const codeEditorService = accessor.get(ICodeEditorService); - - const editor = (codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor()); - if (!editor) { - return false; - } - const controller = InlineChatController.get(editor); - if (!controller) { - return false; - } - const responseContent = controller?.getMessage(); - if (!responseContent) { - return false; - } - accessibleViewService.show({ - verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, - provideContent(): string { return responseContent; }, - onClose() { - controller.focus(); - }, - - options: { type: AccessibleViewType.View } - }); - return true; - }, ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); - } -} - const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatNotebookContribution, LifecyclePhase.Restored); workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatAccessibleViewContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts new file mode 100644 index 00000000000..162d8329ebb --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; + +export class InlineChatAccessibleViewContribution extends Disposable { + static ID: 'inlineChatAccessibleViewContribution'; + constructor() { + super(); + this._register(AccessibleViewAction.addImplementation(100, 'inlineChat', accessor => { + const accessibleViewService = accessor.get(IAccessibleViewService); + const codeEditorService = accessor.get(ICodeEditorService); + + const editor = (codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor()); + if (!editor) { + return false; + } + const controller = InlineChatController.get(editor); + if (!controller) { + return false; + } + const responseContent = controller?.getMessage(); + if (!responseContent) { + return false; + } + accessibleViewService.show({ + verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, + provideContent(): string { return responseContent; }, + onClose() { + controller.focus(); + }, + + options: { type: AccessibleViewType.View } + }); + return true; + }, ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 717d6cf8f59..03802665d4b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -21,15 +21,12 @@ import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/commo import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; import { fromNow } from 'vs/base/common/date'; import { IInlineChatSessionService, Recording } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { runAccessibilityHelpAction } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; import { Disposable } from 'vs/base/common/lifecycle'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { Position } from 'vs/editor/common/core/position'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; @@ -52,23 +49,11 @@ export class StartSessionAction extends EditorAction2 { }); } - private _isInteractivEditorOptions(options: any): options is InlineChatRunOptions { - const { initialSelection, initialRange, message, autoSend, position } = options; - if ( - typeof message !== 'undefined' && typeof message !== 'string' - || typeof autoSend !== 'undefined' && typeof autoSend !== 'boolean' - || typeof initialRange !== 'undefined' && !Range.isIRange(initialRange) - || typeof initialSelection !== 'undefined' && !Selection.isISelection(initialSelection) - || typeof position !== 'undefined' && !Position.isIPosition(position)) { - return false; - } - return true; - } override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) { let options: InlineChatRunOptions | undefined; const arg = _args[0]; - if (arg && this._isInteractivEditorOptions(arg)) { + if (arg && InlineChatRunOptions.isInteractiveEditorOptions(arg)) { options = arg; } InlineChatController.get(editor)?.run(options); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 30c8d290a47..a8c3f04aa1d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -38,7 +38,7 @@ import { Lazy } from 'vs/base/common/lazy'; import { Progress } from 'vs/platform/progress/common/progress'; import { generateUuid } from 'vs/base/common/uuid'; import { TextEdit } from 'vs/editor/common/languages'; -import { ISelection } from 'vs/editor/common/core/selection'; +import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { onUnexpectedError } from 'vs/base/common/errors'; export const enum State { @@ -64,7 +64,7 @@ const enum Message { RERUN_INPUT = 1 << 6, } -export interface InlineChatRunOptions { +export abstract class InlineChatRunOptions { initialSelection?: ISelection; initialRange?: IRange; message?: string; @@ -72,6 +72,19 @@ export interface InlineChatRunOptions { existingSession?: Session; isUnstashed?: boolean; position?: IPosition; + + static isInteractiveEditorOptions(options: any): options is InlineChatRunOptions { + const { initialSelection, initialRange, message, autoSend, position } = options; + if ( + typeof message !== 'undefined' && typeof message !== 'string' + || typeof autoSend !== 'undefined' && typeof autoSend !== 'boolean' + || typeof initialRange !== 'undefined' && !Range.isIRange(initialRange) + || typeof initialSelection !== 'undefined' && !Selection.isISelection(initialSelection) + || typeof position !== 'undefined' && !Position.isIPosition(position)) { + return false; + } + return true; + } } export class InlineChatController implements IEditorContribution { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 370a58cbc05..f925df2421d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -177,10 +177,10 @@ export class InlineChatWidget { private readonly _previewCreateEditor: IdleValue; private readonly _previewCreateModel = this._store.add(new MutableDisposable()); - private readonly _onDidChangeHeight = new MicrotaskEmitter(); + private readonly _onDidChangeHeight = this._store.add(new MicrotaskEmitter()); readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting); - private readonly _onDidChangeInput = new Emitter(); + private readonly _onDidChangeInput = this._store.add(new Emitter()); readonly onDidChangeInput: Event = this._onDidChangeInput.event; private _lastDim: Dimension | undefined; 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 ead402ce540..5c9682417a9 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts @@ -6,7 +6,6 @@ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { isWeb } from 'vs/base/common/platform'; import { mock } from 'vs/base/test/common/mock'; import { assertSnapshot } from 'vs/base/test/common/snapshot'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -20,8 +19,7 @@ import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createNotebookCellList, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; - -(isWeb ? suite.skip : suite)('NotebookEditorStickyScroll', () => { +suite('NotebookEditorStickyScroll', () => { let disposables: DisposableStore; let instantiationService: TestInstantiationService; diff --git a/src/vs/workbench/contrib/output/browser/logViewer.ts b/src/vs/workbench/contrib/output/browser/logViewer.ts index 1eb96fe65f5..417ea949d61 100644 --- a/src/vs/workbench/contrib/output/browser/logViewer.ts +++ b/src/vs/workbench/contrib/output/browser/logViewer.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { dirname, basename } from 'vs/base/common/path'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -12,49 +11,9 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { AbstractTextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; -import { URI } from 'vs/base/common/uri'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { LOG_SCHEME, IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService } from 'vs/platform/files/common/files'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; - -export class LogViewerInput extends TextResourceEditorInput { - - static override readonly ID = 'workbench.editorinputs.output'; - - override get typeId(): string { - return LogViewerInput.ID; - } - - constructor( - outputChannelDescriptor: IFileOutputChannelDescriptor, - @ITextModelService textModelResolverService: ITextModelService, - @ITextFileService textFileService: ITextFileService, - @IEditorService editorService: IEditorService, - @IFileService fileService: IFileService, - @ILabelService labelService: ILabelService, - @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService - ) { - super( - URI.from({ scheme: LOG_SCHEME, path: outputChannelDescriptor.id }), - basename(outputChannelDescriptor.file.path), - dirname(outputChannelDescriptor.file.path), - undefined, - undefined, - textModelResolverService, - textFileService, - editorService, - fileService, - labelService, - filesConfigurationService - ); - } -} export class LogViewer extends AbstractTextResourceEditor { diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 44b292ed5b1..265f3432830 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -10,16 +10,13 @@ import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { MenuId, registerAction2, Action2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { OutputService, LogContentProvider } from 'vs/workbench/contrib/output/browser/outputServices'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_SCHEME, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; +import { OutputService } from 'vs/workbench/contrib/output/browser/outputServices'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor, ACTIVE_OUTPUT_CHANNEL_CONTEXT, IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { OutputViewPane } from 'vs/workbench/contrib/output/browser/outputView'; -import { IEditorPaneRegistry, EditorPaneDescriptor } from 'vs/workbench/browser/editor'; -import { LogViewer, LogViewerInput } from 'vs/workbench/contrib/output/browser/logViewer'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ViewContainer, IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions, IViewsRegistry, IViewsService } from 'vs/workbench/common/views'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; @@ -30,8 +27,8 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { EditorExtensions } from 'vs/workbench/common/editor'; import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; // Register Service registerSingleton(IOutputService, OutputService, InstantiationType.Delayed); @@ -82,25 +79,12 @@ Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews } }], VIEW_CONTAINER); -Registry.as(EditorExtensions.EditorPane).registerEditorPane( - EditorPaneDescriptor.create( - LogViewer, - LogViewer.LOG_VIEWER_EDITOR_ID, - nls.localize('logViewer', "Log Viewer") - ), - [ - new SyncDescriptor(LogViewerInput) - ] -); - class OutputContribution extends Disposable implements IWorkbenchContribution { constructor( @IInstantiationService instantiationService: IInstantiationService, - @ITextModelService textModelService: ITextModelService, @IOutputService private readonly outputService: IOutputService, ) { super(); - textModelService.registerTextModelContentProvider(LOG_SCHEME, instantiationService.createInstance(LogContentProvider)); this.registerActions(); } @@ -293,10 +277,16 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { async run(accessor: ServicesAccessor): Promise { const outputService = accessor.get(IOutputService); const editorService = accessor.get(IEditorService); - const instantiationService = accessor.get(IInstantiationService); + const fileConfigurationService = accessor.get(IFilesConfigurationService); const logFileOutputChannelDescriptor = this.getLogFileOutputChannelDescriptor(outputService); if (logFileOutputChannelDescriptor) { - await editorService.openEditor(instantiationService.createInstance(LogViewerInput, logFileOutputChannelDescriptor), { pinned: true }); + await fileConfigurationService.updateReadonly(logFileOutputChannelDescriptor.file, true); + await editorService.openEditor({ + resource: logFileOutputChannelDescriptor.file, + options: { + pinned: true, + } + }); } } private getLogFileOutputChannelDescriptor(outputService: IOutputService): IFileOutputChannelDescriptor | null { @@ -383,8 +373,8 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { async run(accessor: ServicesAccessor, args?: unknown): Promise { const outputService = accessor.get(IOutputService); const quickInputService = accessor.get(IQuickInputService); - const instantiationService = accessor.get(IInstantiationService); const editorService = accessor.get(IEditorService); + const fileConfigurationService = accessor.get(IFilesConfigurationService); const entries: IOutputChannelQuickPickItem[] = outputService.getChannelDescriptors().filter(c => c.file && c.log) .map(channel => ({ id: channel.id, label: channel.label, channel })); @@ -398,8 +388,14 @@ class OutputContribution extends Disposable implements IWorkbenchContribution { entry = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlogFile', "Select Log File") }); } if (entry) { - assertIsDefined(entry.channel.file); - await editorService.openEditor(instantiationService.createInstance(LogViewerInput, (entry.channel as IFileOutputChannelDescriptor)), { pinned: true }); + const resource = assertIsDefined(entry.channel.file); + await fileConfigurationService.updateReadonly(resource, true); + await editorService.openEditor({ + resource, + options: { + pinned: true, + } + }); } } })); diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 22902b48e47..42d6e2bbd4e 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -9,7 +9,7 @@ import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_LOG_OUTPUT } from 'vs/workbench/services/output/common/output'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry, ACTIVE_OUTPUT_CHANNEL_CONTEXT, CONTEXT_ACTIVE_LOG_OUTPUT } from 'vs/workbench/services/output/common/output'; import { OutputLinkProvider } from 'vs/workbench/contrib/output/browser/outputLinkProvider'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { ITextModel } from 'vs/editor/common/model'; @@ -205,40 +205,3 @@ export class OutputService extends Disposable implements IOutputService, ITextMo } } } - -export class LogContentProvider { - - private channelModels: Map = new Map(); - - constructor( - @IOutputService private readonly outputService: IOutputService, - @IOutputChannelModelService private readonly outputChannelModelService: IOutputChannelModelService, - @ILanguageService private readonly languageService: ILanguageService - ) { - } - - provideTextContent(resource: URI): Promise | null { - if (resource.scheme === LOG_SCHEME) { - const channelModel = this.getChannelModel(resource); - if (channelModel) { - return channelModel.loadModel(); - } - } - return null; - } - - private getChannelModel(resource: URI): IOutputChannelModel | undefined { - const channelId = resource.path; - let channelModel = this.channelModels.get(channelId); - if (!channelModel) { - const channelDisposables: IDisposable[] = []; - const outputChannelDescriptor = this.outputService.getChannelDescriptors().filter(({ id }) => id === channelId)[0]; - if (outputChannelDescriptor && outputChannelDescriptor.file) { - channelModel = this.outputChannelModelService.createOutputChannelModel(channelId, resource, outputChannelDescriptor.languageId ? this.languageService.createById(outputChannelDescriptor.languageId) : this.languageService.createByMimeType(outputChannelDescriptor.log ? LOG_MIME : OUTPUT_MIME), outputChannelDescriptor.file); - channelModel.onDispose(() => dispose(channelDisposables), channelDisposables); - this.channelModels.set(channelId, channelModel); - } - } - return channelModel; - } -} diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index cab8b0d506e..83e168989e4 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -22,7 +22,6 @@ import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteA import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { PlatformName, PlatformToString, isWeb, platform } from 'vs/base/common/platform'; -import { once } from 'vs/base/common/functional'; import { truncate } from 'vs/base/common/strings'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; @@ -768,7 +767,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr quickPick.items = computeItems(); quickPick.sortByLabel = false; quickPick.canSelectMany = false; - once(quickPick.onDidAccept)((async _ => { + Event.once(quickPick.onDidAccept)((async _ => { const selectedItems = quickPick.selectedItems; if (selectedItems.length === 1) { const commandId = selectedItems[0].id!; @@ -793,7 +792,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr } })); - once(quickPick.onDidTriggerItemButton)(async (e) => { + Event.once(quickPick.onDidTriggerItemButton)(async (e) => { const remoteExtension = this.remoteExtensionMetadata.find(value => ExtensionIdentifier.equals(value.id, e.item.id)); if (remoteExtension) { await this.openerService.open(URI.parse(remoteExtension.helpLink)); diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index be89affe5e2..6b0b9db37aa 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -28,7 +28,7 @@ import { IRemoteExplorerService, TunnelType, ITunnelItem, TUNNEL_VIEW_ID, Tunnel import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -518,7 +518,7 @@ class ActionBarRenderer extends Disposable implements ITableRenderer { + const done = createSingleCallFunction(async (success: boolean, finishEditing: boolean) => { dispose(toDispose); if (this.inputDone) { this.inputDone = undefined; diff --git a/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts index 5e0db441a80..43d3c804d40 100644 --- a/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts @@ -14,23 +14,23 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IOpenEvent, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { IViewPaneOptions, ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer'; import { ActionButtonRenderer } from 'vs/workbench/contrib/scm/browser/scmViewPane'; import { getActionViewItemProvider, isSCMActionButton, isSCMRepository, isSCMRepositoryArray } from 'vs/workbench/contrib/scm/browser/util'; -import { ISCMActionButton, ISCMRepository, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMActionButton, ISCMRepository, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, SYNC_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { comparePaths } from 'vs/base/common/comparers'; import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup } from 'vs/workbench/contrib/scm/common/history'; import { localize } from 'vs/nls'; @@ -41,8 +41,18 @@ import { basename, dirname } from 'vs/base/common/resources'; import { ILabelService } from 'vs/platform/label/common/label'; import { stripIcons } from 'vs/base/common/iconLabels'; import { FileKind } from 'vs/platform/files/common/files'; +import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Emitter } from 'vs/base/common/event'; +import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { IResourceNode, ResourceTree } from 'vs/base/common/resourceTree'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -type TreeElement = ISCMRepository[] | ISCMRepository | ISCMActionButton | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | SCMHistoryItemChangeTreeElement; +type SCMHistoryItemChangeResourceTreeNode = IResourceNode; +type TreeElement = ISCMRepository[] | ISCMRepository | ISCMActionButton | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | SCMHistoryItemChangeTreeElement | SCMHistoryItemChangeResourceTreeNode; function isSCMHistoryItemGroupTreeElement(obj: any): obj is SCMHistoryItemGroupTreeElement { return (obj as SCMHistoryItemGroupTreeElement).type === 'historyItemGroup'; @@ -86,11 +96,25 @@ function getSCMResourceId(element: TreeElement): string { const historyItemGroup = historyItem.historyItemGroup; const provider = historyItemGroup.repository.provider; return `historyItemChange:${provider.id}/${historyItemGroup.id}/${historyItem.id}/${element.uri.toString()}`; + } else if (ResourceTree.isResourceNode(element)) { + const historyItem = element.context; + const historyItemGroup = historyItem.historyItemGroup; + const provider = historyItemGroup.repository.provider; + return `folder:${provider.id}/${historyItemGroup.id}/${historyItem.id}/$FOLDER/${element.uri.toString()}`; } else { throw new Error('Invalid tree element'); } } +const enum ViewMode { + List = 'list', + Tree = 'tree' +} + +const ContextKeys = { + ViewMode: new RawContextKey('scmSyncViewMode', ViewMode.List), +}; + interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroup { readonly description?: string; readonly ancestor?: string; @@ -130,12 +154,26 @@ class ListDelegate implements IListVirtualDelegate { return HistoryItemRenderer.TEMPLATE_ID; } else if (isSCMHistoryItemChangeTreeElement(element)) { return HistoryItemChangeRenderer.TEMPLATE_ID; + } else if (ResourceTree.isResourceNode(element)) { + return HistoryItemChangeRenderer.TEMPLATE_ID; } else { throw new Error('Invalid tree element'); } } } +class CompressionDelegate implements ITreeCompressionDelegate { + + isIncompressible(element: TreeElement): boolean { + if (ResourceTree.isResourceNode(element)) { + return element.childrenCount === 0 || !element.parent || !element.parent.parent; + } + + return true; + } + +} + interface HistoryItemGroupTemplate { readonly label: IconLabel; readonly count: CountBadge; @@ -236,12 +274,15 @@ interface HistoryItemChangeTemplate { readonly disposables: IDisposable; } -class HistoryItemChangeRenderer implements ITreeRenderer { +class HistoryItemChangeRenderer implements ICompressibleTreeRenderer { static readonly TEMPLATE_ID = 'historyItemChange'; get templateId(): string { return HistoryItemChangeRenderer.TEMPLATE_ID; } - constructor(private labels: ResourceLabels) { } + constructor( + private readonly viewMode: () => ViewMode, + private readonly labels: ResourceLabels, + @ILabelService private labelService: ILabelService) { } renderTemplate(container: HTMLElement): HistoryItemChangeTemplate { const element = append(container, $('.change')); @@ -252,11 +293,27 @@ class HistoryItemChangeRenderer implements ITreeRenderer, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void { + renderElement(node: ITreeNode, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void { + const historyItemChangeOrFolder = node.element; + const fileKind = ResourceTree.isResourceNode(historyItemChangeOrFolder) && historyItemChangeOrFolder.childrenCount > 0 ? FileKind.FOLDER : FileKind.FILE; + templateData.fileLabel.setFile(node.element.uri, { fileDecorations: { colors: false, badges: true }, - fileKind: FileKind.FILE, - hidePath: false, + fileKind, + hidePath: this.viewMode() === ViewMode.Tree, + }); + } + + renderCompressedElements(node: ITreeNode, void>, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void { + const compressed = node.element as ICompressedTreeNode; + + const folder = compressed.elements[compressed.elements.length - 1]; + const label = compressed.elements.map(e => e.name); + + templateData.fileLabel.setResource({ resource: folder.uri, name: label }, { + fileDecorations: { colors: false, badges: true }, + fileKind: FileKind.FOLDER, + separator: this.labelService.getSeparator(folder.uri.scheme) }); } @@ -292,15 +349,7 @@ class SCMSyncViewPaneAccessibilityProvider implements IListAccessibilityProvider } else if (isSCMHistoryItemTreeElement(element)) { return `${stripIcons(element.label).trim()}${element.description ? `, ${element.description}` : ''}`; } else if (isSCMHistoryItemChangeTreeElement(element)) { - const result: string[] = []; - - result.push(basename(element.uri)); - - // TODO - add decoration - // if (element.decorations.tooltip) { - // result.push(element.decorations.tooltip); - // } - + const result = [basename(element.uri)]; const path = this.labelService.getUriLabel(dirname(element.uri), { relative: true, noPrefix: true }); if (path) { @@ -375,7 +424,7 @@ export class SCMSyncViewPane extends ViewPane { private listLabels!: ResourceLabels; private treeContainer!: HTMLElement; - private _tree!: WorkbenchAsyncDataTree; + private _tree!: WorkbenchCompressibleAsyncDataTree; private _viewModel!: SCMSyncPaneViewModel; get viewModel(): SCMSyncPaneViewModel { return this._viewModel; } @@ -407,24 +456,28 @@ export class SCMSyncViewPane extends ViewPane { this._register(this.listLabels); this._tree = this.instantiationService.createInstance( - WorkbenchAsyncDataTree, + WorkbenchCompressibleAsyncDataTree, 'SCM Sync View', this.treeContainer, new ListDelegate(), + new CompressionDelegate(), [ this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)), this.instantiationService.createInstance(ActionButtonRenderer), this.instantiationService.createInstance(HistoryItemGroupRenderer), this.instantiationService.createInstance(HistoryItemRenderer), - this.instantiationService.createInstance(HistoryItemChangeRenderer, this.listLabels), + this.instantiationService.createInstance(HistoryItemChangeRenderer, () => this.viewModel.mode, this.listLabels), ], - this.instantiationService.createInstance(SCMSyncDataSource), + this.instantiationService.createInstance(SCMSyncDataSource, () => this.viewModel.mode), { + compressionEnabled: true, horizontalScrolling: false, + autoExpandSingleChildren: true, + collapseByDefault: (e) => !ResourceTree.isResourceNode(e), accessibilityProvider: this.instantiationService.createInstance(SCMSyncViewPaneAccessibilityProvider), identityProvider: this.instantiationService.createInstance(SCMSyncViewPaneTreeIdentityProvider), sorter: this.instantiationService.createInstance(SCMSyncViewPaneTreeSorter), - }) as WorkbenchAsyncDataTree; + }) as WorkbenchCompressibleAsyncDataTree; this._register(this._tree); this._register(this._tree.onDidOpen(this.onDidOpen, this)); @@ -436,6 +489,7 @@ export class SCMSyncViewPane extends ViewPane { this.updateIndentStyles(this.themeService.getFileIconTheme()); this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); + this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); } protected override layoutBody(height: number, width: number): void { @@ -443,18 +497,30 @@ export class SCMSyncViewPane extends ViewPane { this._tree.layout(height, width); } + private onDidChangeMode(): void { + this.updateIndentStyles(this.themeService.getFileIconTheme()); + } + private async onDidOpen(e: IOpenEvent): Promise { if (!e.element) { return; - } else if (isSCMHistoryItemChangeTreeElement(e.element)) { + } + + if (isSCMHistoryItemChangeTreeElement(e.element)) { if (e.element.originalUri && e.element.modifiedUri) { await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.uri, e.element.originalUri, e.element.modifiedUri), e); } + } else if (ResourceTree.isResourceNode(e.element) && e.element.childrenCount === 0) { + if (e.element.element?.originalUri && e.element.element?.modifiedUri) { + await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.element.uri, e.element.element.originalUri, e.element.element.modifiedUri), e); + } } } private updateIndentStyles(theme: any): void { - this.treeContainer.classList.toggle('align-icons-and-twisties', theme.hasFileIcons || (theme.hasFileIcons && !theme.hasFolderIcons)); + this.treeContainer.classList.toggle('list-view-mode', this._viewModel.mode === ViewMode.List); + this.treeContainer.classList.toggle('tree-view-mode', this._viewModel.mode === ViewMode.Tree); + this.treeContainer.classList.toggle('align-icons-and-twisties', (this._viewModel.mode === ViewMode.List && theme.hasFileIcons) || (theme.hasFileIcons && !theme.hasFolderIcons)); } override dispose(): void { @@ -465,6 +531,26 @@ export class SCMSyncViewPane extends ViewPane { class SCMSyncPaneViewModel { + private readonly _onDidChangeMode = new Emitter(); + readonly onDidChangeMode = this._onDidChangeMode.event; + + private _mode: ViewMode; + get mode(): ViewMode { return this._mode; } + set mode(mode: ViewMode) { + if (this._mode === mode) { + return; + } + + this._mode = mode; + + this.refresh(); + this.modeContextKey.set(mode); + this._onDidChangeMode.fire(mode); + + this.storageService.store(`scm.syncViewMode`, mode, StorageScope.WORKSPACE, StorageTarget.USER); + } + + private modeContextKey: IContextKey; private repositories = new Map(); private historyProviders = new Map(); @@ -473,19 +559,25 @@ class SCMSyncPaneViewModel { private readonly disposables = new DisposableStore(); constructor( - private readonly tree: WorkbenchAsyncDataTree, + private readonly tree: WorkbenchCompressibleAsyncDataTree, + @IContextKeyService contextKeyService: IContextKeyService, @ISCMViewService scmViewService: ISCMViewService, @IConfigurationService private readonly configurationService: IConfigurationService, - + @IStorageService private readonly storageService: IStorageService, ) { - configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); - this.onDidChangeConfiguration(); + configurationService.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this.disposables); + this._onDidChangeConfiguration(); scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this.disposables); this._onDidChangeVisibleRepositories({ added: scmViewService.visibleRepositories, removed: [] }); + + this._mode = this.getViewMode(); + + this.modeContextKey = ContextKeys.ViewMode.bindTo(contextKeyService); + this.modeContextKey.set(this._mode); } - private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { + private _onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('scm.alwaysShowRepositories')) { this.alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories'); this.refresh(); @@ -524,6 +616,16 @@ class SCMSyncPaneViewModel { } } + private getViewMode(): ViewMode { + let mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewMode.List : ViewMode.Tree; + const storageMode = this.storageService.get(`scm.syncViewMode`, StorageScope.WORKSPACE) as ViewMode; + if (typeof storageMode === 'string') { + mode = storageMode; + } + + return mode; + } + private async refresh(repository?: ISCMRepository): Promise { if (this.repositories.size === 0) { return; @@ -548,6 +650,10 @@ class SCMSyncPaneViewModel { class SCMSyncDataSource implements IAsyncDataSource { + constructor( + private readonly viewMode: () => ViewMode, + @IUriIdentityService private uriIdentityService: IUriIdentityService) { } + hasChildren(element: TreeElement): boolean { if (isSCMRepositoryArray(element)) { return true; @@ -561,6 +667,8 @@ class SCMSyncDataSource implements IAsyncDataSource { return true; } else if (isSCMHistoryItemChangeTreeElement(element)) { return false; + } else if (ResourceTree.isResourceNode(element)) { + return element.childrenCount > 0; } else { throw new Error('hasChildren not implemented.'); } @@ -651,14 +759,35 @@ class SCMSyncDataSource implements IAsyncDataSource { // History Item Changes const changes = await historyProvider.provideHistoryItemChanges(element.id) ?? []; - children.push(...changes.map(change => ({ - uri: change.uri, - originalUri: change.originalUri, - modifiedUri: change.modifiedUri, - renameUri: change.renameUri, - historyItem: element, - type: 'historyItemChange' - } as SCMHistoryItemChangeTreeElement))); + + if (this.viewMode() === ViewMode.List) { + // List + children.push(...changes.map(change => ({ + uri: change.uri, + originalUri: change.originalUri, + modifiedUri: change.modifiedUri, + renameUri: change.renameUri, + historyItem: element, + type: 'historyItemChange' + } as SCMHistoryItemChangeTreeElement))); + } else { + // Tree + const tree = new ResourceTree(element, repository.provider.rootUri ?? URI.file('/'), this.uriIdentityService.extUri); + for (const change of changes) { + tree.add(change.uri, { + uri: change.uri, + originalUri: change.originalUri, + modifiedUri: change.modifiedUri, + renameUri: change.renameUri, + historyItem: element, + type: 'historyItemChange' + } as SCMHistoryItemChangeTreeElement); + } + + children.push(...tree.root.children); + } + } else if (ResourceTree.isResourceNode(element)) { + children.push(...element.children); } else { throw new Error('getChildren Method not implemented.'); } @@ -666,3 +795,50 @@ class SCMSyncDataSource implements IAsyncDataSource { return children; } } + +class SetListViewModeAction extends ViewAction { + + constructor() { + super({ + id: 'workbench.scm.sync.action.setListViewMode', + title: localize('setListViewMode', "View as List"), + viewId: SYNC_VIEW_PANE_ID, + f1: false, + icon: Codicon.listTree, + toggled: ContextKeys.ViewMode.isEqualTo(ViewMode.List), + menu: { + id: MenuId.ViewTitle, + group: '1_viewmode' + } + }); + } + + async runInView(_: ServicesAccessor, view: SCMSyncViewPane): Promise { + view.viewModel.mode = ViewMode.List; + } +} + +class SetTreeViewModeAction extends ViewAction { + + constructor() { + super({ + id: 'workbench.scm.sync.action.setTreeViewMode', + title: localize('setTreeViewMode', "View as Tree"), + viewId: SYNC_VIEW_PANE_ID, + f1: false, + icon: Codicon.listFlat, + toggled: ContextKeys.ViewMode.isEqualTo(ViewMode.Tree), + menu: { + id: MenuId.ViewTitle, + group: '1_viewmode' + } + }); + } + + async runInView(_: ServicesAccessor, view: SCMSyncViewPane): Promise { + view.viewModel.mode = ViewMode.Tree; + } +} + +registerAction2(SetListViewModeAction); +registerAction2(SetTreeViewModeAction); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 34016a59465..c08eb656f43 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -221,6 +221,8 @@ class SCMTreeDragAndDrop implements ITreeDragAndDrop { } return uris; } + + dispose(): void { } } interface InputTemplate { diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 94fc88fe959..35317213619 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -45,7 +45,7 @@ import { IWorkbenchQuickAccessConfiguration } from 'vs/workbench/browser/quickac import { GotoSymbolQuickAccessProvider } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ScrollType, IEditor, ICodeEditorViewState, IDiffEditorViewState } from 'vs/editor/common/editorCommon'; -import { once } from 'vs/base/common/functional'; +import { Event } from 'vs/base/common/event'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { getIEditor } from 'vs/editor/browser/editorBrowser'; import { Codicon } from 'vs/base/common/codicons'; @@ -112,7 +112,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + Event.once(picker.onDispose)(() => { if (picker === this.picker) { this.picker = undefined; // clear the picker when disposed to not keep it in memory for too long } @@ -236,7 +236,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + disposables.add(Event.once(picker.onDidHide)(({ reason }) => { if (reason === QuickInputHideReason.Gesture) { this.pickState.restoreEditorViewState(); } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 81cb217e4b6..cffbd00a060 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -62,7 +62,6 @@ import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDe import { raceTimeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { once } from 'vs/base/common/functional'; import { toFormattedString } from 'vs/base/common/jsonFormatter'; import { Schemas } from 'vs/base/common/network'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -340,7 +339,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } })); this._waitForAllSupportedExecutions = new Promise(resolve => { - once(this._onDidRegisterAllSupportedExecutions.event)(() => resolve()); + Event.once(this._onDidRegisterAllSupportedExecutions.event)(() => resolve()); }); if (this._terminalService.getReconnectedTerminals('Task')?.length) { this._attemptTaskReconnection(); diff --git a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts index a83ec7b1be1..df714767124 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { Codicon } from 'vs/base/common/codicons'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; import { AbstractProblemCollector, StartStopProblemCollector } from 'vs/workbench/contrib/tasks/common/problemCollectors'; import { ITaskGeneralEvent, ITaskProcessEndedEvent, ITaskProcessStartedEvent, TaskEventKind, TaskRunType } from 'vs/workbench/contrib/tasks/common/tasks'; @@ -23,7 +23,7 @@ interface ITerminalData { status: ITerminalStatus; problemMatcher: AbstractProblemCollector; taskRunEnded: boolean; - disposeListener?: IDisposable; + disposeListener?: MutableDisposable; } const TASK_TERMINAL_STATUS_ID = 'task_terminal_status'; @@ -50,6 +50,12 @@ export class TaskTerminalStatus extends Disposable { case TaskEventKind.ProcessEnded: this.eventEnd(event); break; } })); + this._register(toDisposable(() => { + for (const terminalData of this.terminalMap.values()) { + terminalData.disposeListener?.dispose(); + } + this.terminalMap.clear(); + })); } addTerminal(task: Task, terminal: ITerminalInstance, problemMatcher: AbstractProblemCollector) { @@ -57,6 +63,9 @@ export class TaskTerminalStatus extends Disposable { terminal.statusList.add(status); this._register(problemMatcher.onDidFindFirstMatch(() => { this._marker = terminal.registerMarker(); + if (this._marker) { + this._register(this._marker); + } })); this._register(problemMatcher.onDidFindErrors(() => { if (this._marker) { @@ -129,7 +138,8 @@ export class TaskTerminalStatus extends Disposable { return; } if (!terminalData.disposeListener) { - terminalData.disposeListener = terminalData.terminal.onDisposed(() => { + terminalData.disposeListener = this._register(new MutableDisposable()); + terminalData.disposeListener.value = terminalData.terminal.onDisposed(() => { if (!event.terminalId) { return; } diff --git a/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts b/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts index 6de24a38863..faaa27efe17 100644 --- a/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts +++ b/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts @@ -5,7 +5,7 @@ import { ok } from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -34,8 +34,14 @@ class TestAudioCueService implements Partial { } } -class TestTerminal implements Partial { - statusList: TerminalStatusList = new TerminalStatusList(new TestConfigurationService()); +class TestTerminal extends Disposable implements Partial { + statusList: TerminalStatusList = this._register(new TerminalStatusList(new TestConfigurationService())); + constructor() { + super(); + } + override dispose(): void { + super.dispose(); + } } class TestTask extends CommonTask { @@ -52,7 +58,7 @@ class TestTask extends CommonTask { } } -class TestProblemCollector implements Partial { +class TestProblemCollector extends Disposable implements Partial { protected readonly _onDidFindFirstMatch = new Emitter(); readonly onDidFindFirstMatch = this._onDidFindFirstMatch.event; protected readonly _onDidFindErrors = new Emitter(); @@ -62,7 +68,6 @@ class TestProblemCollector implements Partial { } suite('Task Terminal Status', () => { - let store: DisposableStore; let instantiationService: TestInstantiationService; let taskService: TestTaskService; let taskTerminalStatus: TaskTerminalStatus; @@ -70,22 +75,16 @@ suite('Task Terminal Status', () => { let testTask: Task; let problemCollector: AbstractProblemCollector; let audioCueService: TestAudioCueService; + const store = ensureNoDisposablesAreLeakedInTestSuite(); setup(() => { - store = new DisposableStore(); - instantiationService = new TestInstantiationService(); + instantiationService = store.add(new TestInstantiationService()); taskService = new TestTaskService(); audioCueService = new TestAudioCueService(); - taskTerminalStatus = new TaskTerminalStatus(taskService as any, audioCueService as any); - testTerminal = instantiationService.createInstance(TestTerminal) as any; + taskTerminalStatus = store.add(new TaskTerminalStatus(taskService as any, audioCueService as any)); + testTerminal = store.add(instantiationService.createInstance(TestTerminal) as any); testTask = instantiationService.createInstance(TestTask) as unknown as Task; - problemCollector = instantiationService.createInstance(TestProblemCollector) as any; - store.add(instantiationService); - store.add(taskTerminalStatus); + problemCollector = store.add(instantiationService.createInstance(TestProblemCollector) as any); }); - teardown(() => { - store.clear(); - }); - ensureNoDisposablesAreLeakedInTestSuite(); test('Should add failed status when there is an exit code on task end', async () => { taskTerminalStatus.addTerminal(testTask, testTerminal, problemCollector); taskService.triggerStateChange({ kind: TaskEventKind.ProcessStarted }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index c04432f8b2f..a25787349e1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -35,7 +35,7 @@ import { getColorClass, getIconId, getUriClasses } from 'vs/workbench/contrib/te import { IEditableData } from 'vs/workbench/common/views'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { CodeDataTransfers, containsDragType } from 'vs/platform/dnd/browser/dnd'; @@ -81,7 +81,6 @@ export class TerminalTabList extends WorkbenchList { @ILifecycleService lifecycleService: ILifecycleService, @IHoverService private readonly _hoverService: IHoverService, ) { - const dnd = instantiationService.createInstance(TerminalTabsDragAndDrop); super('TerminalTabsList', container, { getHeight: () => TerminalTabsListSizes.TabHeight, @@ -99,7 +98,7 @@ export class TerminalTabList extends WorkbenchList { smoothScrolling: _configurationService.getValue('workbench.list.smoothScrolling'), multipleSelectionSupport: true, paddingBottom: TerminalTabsListSizes.TabHeight, - dnd, + dnd: instantiationService.createInstance(TerminalTabsDragAndDrop), openOnSingleClick: true }, contextKeyService, @@ -107,7 +106,6 @@ export class TerminalTabList extends WorkbenchList { _configurationService, instantiationService, ); - this.disposables.add(dnd); const instanceDisposables: IDisposable[] = [ this._terminalGroupService.onDidChangeInstances(() => this.refresh()), @@ -419,7 +417,7 @@ class TerminalTabsRenderer implements IListRenderer { + const done = createSingleCallFunction((success: boolean, finishEditing: boolean) => { inputBox.element.style.display = 'none'; const value = inputBox.value; dispose(toDispose); diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index 3a4563396db..221c139fc2b 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -6,7 +6,7 @@ import { findFirstIdxMonotonousOrArrLen } from 'vs/base/common/arraysFind'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { Disposable, DisposableStore, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { generateUuid } from 'vs/base/common/uuid'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -98,7 +98,7 @@ export class TestResultService extends Disposable implements ITestResultService private readonly isRunning: IContextKey; private readonly hasAnyResults: IContextKey; - private readonly loadResults = once(() => this.storage.read().then(loaded => { + private readonly loadResults = createSingleCallFunction(() => this.storage.read().then(loaded => { for (let i = loaded.length - 1; i >= 0; i--) { this.push(loaded[i]); } 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 773842399ae..3ff85835829 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts @@ -23,7 +23,7 @@ suite('Getting Started Markdown Renderer', () => { const rendered = await renderer.renderMarkdown(mdPath, mdBase); const imageSrcs = [...rendered.matchAll(/img src="[^"]*"/g)].map(match => match[0]); for (const src of imageSrcs) { - const targetSrcFormat = /^img src="https:\/\/file\+.vscode-resource.vscode-cdn.net\/.*\/vs\/workbench\/contrib\/welcomeGettingStarted\/common\/media\/.*.png"$/; + const targetSrcFormat = /^img src=".*\/vs\/workbench\/contrib\/welcomeGettingStarted\/common\/media\/.*.png"$/; assert(targetSrcFormat.test(src), `${src} didnt match regex`); } languageService.dispose(); diff --git a/src/vs/workbench/services/clipboard/browser/clipboardService.ts b/src/vs/workbench/services/clipboard/browser/clipboardService.ts index 21ecf79467e..6abdf43e7d2 100644 --- a/src/vs/workbench/services/clipboard/browser/clipboardService.ts +++ b/src/vs/workbench/services/clipboard/browser/clipboardService.ts @@ -9,7 +9,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { BrowserClipboardService as BaseBrowserClipboardService } from 'vs/platform/clipboard/browser/clipboardService'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { once } from 'vs/base/common/functional'; +import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -62,7 +62,7 @@ export class BrowserClipboardService extends BaseBrowserClipboardService { ); // Always resolve the promise once the notification closes - listener.add(once(handle.onDidClose)(() => resolve(''))); + listener.add(Event.once(handle.onDidClose)(() => resolve(''))); }); } } diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index e05e7d2519c..f72156fa7b8 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -12,7 +12,7 @@ import { getZoomFactor } from 'vs/base/browser/browser'; import { unmnemonicLabel } from 'vs/base/common/labels'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IContextMenuDelegate, IContextMenuEvent } from 'vs/base/browser/contextmenu'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; import { popup } from 'vs/base/parts/contextmenu/electron-sandbox/contextmenu'; import { getTitleBarStyle } from 'vs/platform/window/common/window'; @@ -93,7 +93,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService const actions = delegate.getActions(); if (actions.length) { - const onHide = once(() => { + const onHide = createSingleCallFunction(() => { delegate.onHide?.(false); dom.ModifierKeyEmitter.getInstance().resetKeyStatus(); 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 e2881cf20cc..2648fb5da20 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -1455,6 +1455,28 @@ suite('EditorGroupsService', () => { editorGroupModelChangeListener.dispose(); }); + test('sticky: true wins over index', async () => { + const [part] = await createPart(); + const group = part.activeGroup; + + assert.strictEqual(group.stickyCount, 0); + + const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = createTestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + const inputSticky = createTestFileEditorInput(URI.file('foo/bar/sticky'), TEST_EDITOR_INPUT_ID); + + await group.openEditor(input, { pinned: true }); + await group.openEditor(inputInactive, { inactive: true }); + await group.openEditor(inputSticky, { sticky: true, index: 2 }); + + assert.strictEqual(group.stickyCount, 1); + assert.strictEqual(group.isSticky(inputSticky), true); + + assert.strictEqual(group.getIndexOfEditor(input), 1); + assert.strictEqual(group.getIndexOfEditor(inputInactive), 2); + assert.strictEqual(group.getIndexOfEditor(inputSticky), 0); + }); + test('moveEditor with context (across groups)', async () => { const [part] = await createPart(); const group = part.activeGroup; diff --git a/src/vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler.ts b/src/vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler.ts index 8b380b9a887..a5907cf52d0 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler.ts @@ -9,7 +9,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IV8InspectProfilingService, IV8Profile, IV8ProfileNode } from 'vs/platform/profiling/common/profiling'; -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; export class ExtensionHostProfiler { @@ -25,7 +25,7 @@ export class ExtensionHostProfiler { const id = await this._profilingService.startProfiling({ port: this._port }); return { - stop: once(async () => { + stop: createSingleCallFunction(async () => { const profile = await this._profilingService.stopProfiling(id); await this._extensionService.whenInstalledExtensionsRegistered(); const extensions = this._extensionService.extensions; diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index 84548be4a88..4ef30ef4e9d 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -29,11 +29,6 @@ export const OUTPUT_MODE_ID = 'Log'; */ export const LOG_MIME = 'text/x-code-log-output'; -/** - * Log resource scheme. - */ -export const LOG_SCHEME = 'log'; - /** * Id used by the log output editor. */ diff --git a/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts b/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts index 700329e7361..b9bb5be6608 100644 --- a/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts +++ b/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { once } from 'vs/base/common/functional'; +import { createSingleCallFunction } from 'vs/base/common/functional'; import { isLinux } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { localize } from 'vs/nls'; @@ -52,7 +52,7 @@ export class NativeSecretStorageService extends BaseSecretStorageService { return super.set(key, value); } - private notifyOfNoEncryptionOnce = once(() => this.notifyOfNoEncryption()); + private notifyOfNoEncryptionOnce = createSingleCallFunction(() => this.notifyOfNoEncryption()); private async notifyOfNoEncryption(): Promise { const buttons: IPromptChoice[] = []; const troubleshootingButton: IPromptChoice = { diff --git a/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css b/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css index b17aa5fb4d7..568c085b01f 100644 --- a/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css +++ b/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css @@ -102,6 +102,10 @@ align-items: center; } +.profile-edit-widget > .profile-icon-container:only-child > .profile-icon-label { + width: 45px; +} + .profile-edit-widget > .profile-icon-container > .profile-icon-id { display: inline-flex; align-items: center; diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 1a62f5f9cd6..e5db2424547 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -11,7 +11,7 @@ import { ITelemetryData, ITelemetryService, TelemetryLevel } from 'vs/platform/t import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorsOrder, IFileEditorInput, IEditorFactoryRegistry, IEditorSerializer, EditorExtensions, ISaveOptions, IMoveResult, ITextDiffEditorPane, IVisibleEditorPane, IEditorOpenContext, EditorExtensions as Extensions, EditorInputCapabilities, IUntypedEditorInput, IEditorWillMoveEvent, IEditorWillOpenEvent, IActiveEditorChangeEvent, EditorPaneSelectionChangeReason, IEditorPaneSelection } from 'vs/workbench/common/editor'; -import { EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor, IEditorGroupTitleHeight } from 'vs/workbench/browser/parts/editor/editor'; +import { EditorServiceImpl, IEditorGroupView, IEditorGroupsView, IEditorGroupTitleHeight } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; import { IResolvedWorkingCopyBackup, IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IConfigurationService, ConfigurationTarget, IConfigurationValue } from 'vs/platform/configuration/common/configuration'; @@ -932,7 +932,7 @@ export class TestEditorGroupView implements IEditorGroupView { relayout() { } } -export class TestEditorGroupAccessor implements IEditorGroupsAccessor { +export class TestEditorGroupAccessor implements IEditorGroupsView { groups: IEditorGroupView[] = []; activeGroup!: IEditorGroupView; diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index 5d19c5e5205..5084f654f29 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -17,6 +17,9 @@ const minimatch = require('minimatch'); const fs = require('fs'); const playwright = require('@playwright/test'); const { applyReporter } = require('../reporter'); +const yaserver = require('yaserver'); +const http = require('http'); +const { randomBytes } = require('crypto'); // opts const defaultReporterName = process.platform === 'win32' ? 'list' : 'spec'; @@ -60,7 +63,8 @@ const withReporter = (function () { })(); const outdir = argv.build ? 'out-build' : 'out'; -const out = path.join(__dirname, `../../../${outdir}`); +const rootDir = path.resolve(__dirname, '..', '..', '..'); +const out = path.join(rootDir, `${outdir}`); function ensureIsArray(a) { return Array.isArray(a) ? a : [a]; @@ -126,34 +130,80 @@ function consoleLogFn(msg) { return console.log; } +async function createServer() { + // Demand a prefix to avoid issues with other services on the + // machine being able to access the test server. + const prefix = '/' + randomBytes(16).toString('hex'); + const serveStatic = await yaserver.createServer({ rootDir }); + + /** Handles a request for a remote method call, invoking `fn` and returning the result */ + const remoteMethod = async (/** @type {http.IncomingMessage} */ req, /** @type {http.ServerResponse} */ response, fn) => { + const params = await new Promise((resolve, reject) => { + const body = []; + req.on('data', chunk => body.push(chunk)); + req.on('end', () => resolve(JSON.parse(Buffer.concat(body).toString()))); + req.on('error', reject); + }); + + const result = await fn(...params); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify(result)); + }; + + const server = http.createServer((request, response) => { + if (!request.url?.startsWith(prefix)) { + return response.writeHead(404).end(); + } + + // rewrite the URL so the static server can handle the request correctly + request.url = request.url.slice(prefix.length); + + switch (request.url) { + case '/remoteMethod/__readFileInTests': + return remoteMethod(request, response, p => fs.promises.readFile(p, 'utf-8')); + case '/remoteMethod/__writeFileInTests': + return remoteMethod(request, response, (p, contents) => fs.promises.writeFile(p, contents)); + case '/remoteMethod/__readDirInTests': + return remoteMethod(request, response, p => fs.promises.readdir(p)); + case '/remoteMethod/__unlinkInTests': + return remoteMethod(request, response, p => fs.promises.unlink(p)); + case '/remoteMethod/__mkdirPInTests': + return remoteMethod(request, response, p => fs.promises.mkdir(p, { recursive: true })); + default: + return serveStatic.handle(request, response); + } + }); + + return new Promise((resolve, reject) => { + server.listen(0, 'localhost', () => { + resolve({ + dispose: () => server.close(), + // @ts-ignore + url: `http://localhost:${server.address().port}${prefix}` + }); + }); + server.on('error', reject); + }); +} + async function runTestsInBrowser(testModules, browserType) { + const server = await createServer(); const browser = await playwright[browserType].launch({ headless: !Boolean(argv.debug), devtools: Boolean(argv.debug) }); const context = await browser.newContext(); const page = await context.newPage(); - const target = url.pathToFileURL(path.join(__dirname, 'renderer.html')); + const target = new URL(server.url + '/test/unit/browser/renderer.html'); + target.searchParams.set('baseUrl', url.pathToFileURL(path.join(rootDir, 'src')).toString()); if (argv.build) { - if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { - target.search = `?build=true&ci=true`; - } else { - target.search = `?build=true`; - } - } else if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { - target.search = `?ci=true`; + target.searchParams.set('build', 'true'); + } + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + target.searchParams.set('ci', 'true'); } const emitter = new events.EventEmitter(); - - await Promise.all([ - page.exposeFunction('mocha_report', (type, data1, data2) => { - emitter.emit(type, data1, data2); - }), - // Test file operations that are common across platforms. Used for test infra, namely snapshot tests - page.exposeFunction('__readFileInTests', (path) => fs.promises.readFile(path, 'utf-8')), - page.exposeFunction('__writeFileInTests', (path, contents) => fs.promises.writeFile(path, contents)), - page.exposeFunction('__readDirInTests', (path) => fs.promises.readdir(path)), - page.exposeFunction('__unlinkInTests', (path) => fs.promises.unlink(path)), - page.exposeFunction('__mkdirPInTests', (path) => fs.promises.mkdir(path, { recursive: true })), - ]); + await page.exposeFunction('mocha_report', (type, data1, data2) => { + emitter.emit(type, data1, data2); + }); await page.goto(target.href); @@ -190,6 +240,7 @@ async function runTestsInBrowser(testModules, browserType) { } catch (err) { console.error(err); } + server.dispose(); await browser.close(); if (failingTests.length > 0) { diff --git a/test/unit/browser/renderer.html b/test/unit/browser/renderer.html index fe62a749a68..ae678696483 100644 --- a/test/unit/browser/renderer.html +++ b/test/unit/browser/renderer.html @@ -51,7 +51,7 @@ const baseUrl = window.location.href; require.config({ catchError: true, - baseUrl: new URL('../../../src', baseUrl).href, + baseUrl: urlParams.get('baseUrl'), paths: { vs: new URL(`../../../${!!isBuild ? 'out-build' : 'out'}/vs`, baseUrl).href, assert: new URL('../assert.js', baseUrl).href, @@ -116,6 +116,27 @@ runner.on('pending', test => window.mocha_report('pending', serializeRunnable(test))); }; + const remoteMethods = [ + '__readFileInTests', + '__writeFileInTests', + '__readDirInTests', + '__unlinkInTests', + '__mkdirPInTests', + ]; + + for (const method of remoteMethods) { + const prefix = window.location.pathname.split('/')[1]; + globalThis[method] = async (...args) => { + const res = await fetch(`/${prefix}/remoteMethod/${method}`, { + body: JSON.stringify(args), + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + return res.json(); + } + } + async function loadModules(modules) { for (const file of modules) { @@ -152,7 +173,8 @@ }); } - const modules = new URL(window.location.href).searchParams.getAll('m'); + const url = new URL(window.location.href); + const modules = url.searchParams.getAll('m'); if (Array.isArray(modules) && modules.length > 0) { console.log('MANUALLY running tests', modules); diff --git a/yarn.lock b/yarn.lock index 02ee18f6e8b..468203799a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10878,10 +10878,10 @@ yargs@^7.1.0: y18n "^3.2.1" yargs-parser "5.0.0-security.0" -yaserver@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/yaserver/-/yaserver-0.2.0.tgz#56393027dc13f3c1bb89d20e0bd17269aa927802" - integrity sha512-onsELrl7Y42M4P3T9R0N/ZJNJRu4cGwzhDyOWIFRMJvPUIrGKInYGh+DJBefrbr1qoyDu7DSCLl9BL5hSSVfDA== +yaserver@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/yaserver/-/yaserver-0.4.0.tgz#71b5fc53fb14c0f241d2dcfb3910707feeb619da" + integrity sha512-98Vj4sgqB1fLcpf2wK7h3dFCaabISHU9CXZHaAx3QLkvTTCD31MzMcNbw5V5jZFBK7ffkFqfWig6B20KQt4wtA== yauzl@^2.10.0, yauzl@^2.4.2, yauzl@^2.9.2: version "2.10.0"