diff --git a/extensions/git-base/src/api/api1.ts b/extensions/git-base/src/api/api1.ts index 7b261f10683..005a7930356 100644 --- a/extensions/git-base/src/api/api1.ts +++ b/extensions/git-base/src/api/api1.ts @@ -5,9 +5,9 @@ import { Disposable, commands } from 'vscode'; import { Model } from '../model'; -import { pickRemoteSource } from '../remoteSource'; +import { getRemoteSourceActions, pickRemoteSource } from '../remoteSource'; import { GitBaseExtensionImpl } from './extension'; -import { API, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceProvider } from './git-base'; +import { API, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction, RemoteSourceProvider } from './git-base'; export class ApiImpl implements API { @@ -17,6 +17,10 @@ export class ApiImpl implements API { return pickRemoteSource(this._model, options as any); } + getRemoteSourceActions(url: string): Promise { + return getRemoteSourceActions(this._model, url); + } + registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable { return this._model.registerRemoteSourceProvider(provider); } diff --git a/extensions/git-base/src/api/git-base.d.ts b/extensions/git-base/src/api/git-base.d.ts index 8510df6d043..53cac4d5c70 100644 --- a/extensions/git-base/src/api/git-base.d.ts +++ b/extensions/git-base/src/api/git-base.d.ts @@ -44,6 +44,15 @@ export interface PickRemoteSourceResult { readonly branch?: string; } +export interface RemoteSourceAction { + readonly label: string; + /** + * Codicon name + */ + readonly icon: string; + run(branch: string): void; +} + export interface RemoteSource { readonly name: string; readonly description?: string; @@ -70,6 +79,7 @@ export interface RemoteSourceProvider { readonly supportsQuery?: boolean; getBranches?(url: string): ProviderResult; + getRemoteSourceActions?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } diff --git a/extensions/git-base/src/remoteSource.ts b/extensions/git-base/src/remoteSource.ts index d7fb85c620f..05831eb010b 100644 --- a/extensions/git-base/src/remoteSource.ts +++ b/extensions/git-base/src/remoteSource.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { QuickPickItem, window, QuickPick, QuickPickItemKind, l10n } from 'vscode'; -import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult } from './api/git-base'; +import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction } from './api/git-base'; import { Model } from './model'; import { throttle, debounce } from './decorators'; @@ -81,6 +81,20 @@ class RemoteSourceProviderQuickPick { } } +export async function getRemoteSourceActions(model: Model, url: string): Promise { + const providers = model.getRemoteProviders(); + + const remoteSourceActions = []; + for (const provider of providers) { + const providerActions = await provider.getRemoteSourceActions?.(url); + if (providerActions?.length) { + remoteSourceActions.push(...providerActions); + } + } + + return remoteSourceActions; +} + export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise; export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch: true }): Promise; export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise { diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index e1fcc022aa6..375d07c4198 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -5,7 +5,7 @@ import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; -import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider } from './git'; +import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions } from './git'; import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode'; import { combinedDisposable, mapEvent } from '../util'; import { toGitUri } from '../uri'; @@ -294,9 +294,9 @@ export class ApiImpl implements API { return result ? new ApiRepository(result) : null; } - async init(root: Uri): Promise { + async init(root: Uri, options?: InitOptions): Promise { const path = root.fsPath; - await this._model.git.init(path); + await this._model.git.init(path, options); await this._model.openRepository(path); return this.getRepository(root) || null; } @@ -362,6 +362,7 @@ function getStatus(status: Status): string { case Status.UNTRACKED: return 'UNTRACKED'; case Status.IGNORED: return 'IGNORED'; case Status.INTENT_TO_ADD: return 'INTENT_TO_ADD'; + case Status.INTENT_TO_RENAME: return 'INTENT_TO_RENAME'; case Status.ADDED_BY_US: return 'ADDED_BY_US'; case Status.ADDED_BY_THEM: return 'ADDED_BY_THEM'; case Status.DELETED_BY_US: return 'DELETED_BY_US'; diff --git a/extensions/git/src/api/git-base.d.ts b/extensions/git/src/api/git-base.d.ts index dca68d13071..1eeb1739901 100644 --- a/extensions/git/src/api/git-base.d.ts +++ b/extensions/git/src/api/git-base.d.ts @@ -8,6 +8,7 @@ export { ProviderResult } from 'vscode'; export interface API { pickRemoteSource(options: PickRemoteSourceOptions): Promise; + getRemoteSourceActions(url: string): Promise; registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; } @@ -31,9 +32,12 @@ export interface GitBaseExtension { export interface PickRemoteSourceOptions { readonly providerLabel?: (provider: RemoteSourceProvider) => string; - readonly urlLabel?: string; + readonly urlLabel?: string | ((url: string) => string); readonly providerName?: string; + readonly title?: string; + readonly placeholder?: string; readonly branch?: boolean; // then result is PickRemoteSourceResult + readonly showRecentSources?: boolean; } export interface PickRemoteSourceResult { @@ -41,20 +45,42 @@ export interface PickRemoteSourceResult { readonly branch?: string; } +export interface RemoteSourceAction { + readonly label: string; + /** + * Codicon name + */ + readonly icon: string; + run(branch: string): void; +} + export interface RemoteSource { readonly name: string; readonly description?: string; + readonly detail?: string; + /** + * Codicon name + */ + readonly icon?: string; readonly url: string | string[]; } +export interface RecentRemoteSource extends RemoteSource { + readonly timestamp: number; +} + export interface RemoteSourceProvider { readonly name: string; /** * Codicon name */ readonly icon?: string; + readonly label?: string; + readonly placeholder?: string; readonly supportsQuery?: boolean; getBranches?(url: string): ProviderResult; + getRemoteSourceActions?(url: string): ProviderResult; + getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index e3ec7c1000d..30b8c271103 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -78,6 +78,7 @@ export const enum Status { UNTRACKED, IGNORED, INTENT_TO_ADD, + INTENT_TO_RENAME, ADDED_BY_US, ADDED_BY_THEM, @@ -156,6 +157,10 @@ export interface FetchOptions { depth?: number; } +export interface InitOptions { + defaultBranch?: string; +} + export interface RefQuery { readonly contains?: string; readonly count?: number; @@ -307,7 +312,7 @@ export interface API { toGitUri(uri: Uri, ref: string): Uri; getRepository(uri: Uri): Repository | null; - init(root: Uri): Promise; + init(root: Uri, options?: InitOptions): Promise; openRepository(root: Uri): Promise registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable; diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 624e9d2353c..cdf81dc6e61 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,7 +5,7 @@ import * as os from 'os'; import * as path from 'path'; -import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind } from 'vscode'; +import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import { Branch, ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote } from './api/git'; @@ -17,7 +17,8 @@ import { fromGitUri, toGitUri, isGitUri, toMergeUris } from './uri'; import { grep, isDescendant, pathEquals, relativePath } from './util'; import { GitTimelineItem } from './timelineProvider'; import { ApiRepository } from './api/api1'; -import { pickRemoteSource } from './remoteSource'; +import { getRemoteSourceActions, pickRemoteSource } from './remoteSource'; +import { RemoteSourceAction } from './api/git-base'; class CheckoutItem implements QuickPickItem { @@ -25,8 +26,11 @@ class CheckoutItem implements QuickPickItem { get label(): string { return `${this.repository.isBranchProtected(this.ref) ? '$(lock)' : '$(git-branch)'} ${this.ref.name || this.shortCommit}`; } get description(): string { return this.shortCommit; } get refName(): string | undefined { return this.ref.name; } + get refRemote(): string | undefined { return this.ref.remote; } + get buttons(): QuickInputButton[] | undefined { return this._buttons; } + set buttons(newButtons: QuickInputButton[] | undefined) { this._buttons = newButtons; } - constructor(protected repository: Repository, protected ref: Ref) { } + constructor(protected repository: Repository, protected ref: Ref, protected _buttons?: QuickInputButton[]) { } async run(opts?: { detached?: boolean }): Promise { if (!this.ref.name) { @@ -278,7 +282,54 @@ async function createCheckoutItems(repository: Repository, detached = false): Pr } } - return processors.reduce((r, p) => r.concat(...p.items), []); + const buttons = await getRemoteRefItemButtons(repository); + let fallbackRemoteButtons: RemoteSourceActionButton[] | undefined = []; + const remote = repository.remotes.find(r => r.pushUrl === repository.HEAD?.remote || r.fetchUrl === repository.HEAD?.remote) ?? repository.remotes[0]; + const remoteUrl = remote.pushUrl ?? remote.fetchUrl; + if (remoteUrl) { + fallbackRemoteButtons = buttons.get(remoteUrl); + } + + return processors.reduce((r, p) => r.concat(...p.items.map((item) => { + if (item.refRemote) { + const matchingRemote = repository.remotes.find((remote) => remote.name === item.refRemote); + const remoteUrl = matchingRemote?.pushUrl ?? matchingRemote?.fetchUrl; + if (remoteUrl) { + item.buttons = buttons.get(item.refRemote); + } + } + + item.buttons = fallbackRemoteButtons; + return item; + })), []); +} + +type RemoteSourceActionButton = { + iconPath: ThemeIcon; + tooltip: string; + actual: RemoteSourceAction; +}; + +async function getRemoteRefItemButtons(repository: Repository) { + // Compute actions for all known remotes + const remoteUrlsToActions = new Map(); + + const getButtons = async (remoteUrl: string) => (await getRemoteSourceActions(remoteUrl)).map((action) => ({ iconPath: new ThemeIcon(action.icon), tooltip: action.label, actual: action })); + + for (const remote of repository.remotes) { + if (remote.fetchUrl) { + const actions = remoteUrlsToActions.get(remote.fetchUrl) ?? []; + actions.push(...await getButtons(remote.fetchUrl)); + remoteUrlsToActions.set(remote.fetchUrl, actions); + } + if (remote.pushUrl && remote.pushUrl !== remote.fetchUrl) { + const actions = remoteUrlsToActions.get(remote.pushUrl) ?? []; + actions.push(...await getButtons(remote.pushUrl)); + remoteUrlsToActions.set(remote.pushUrl, actions); + } + } + + return remoteUrlsToActions; } class CheckoutProcessor { @@ -2084,7 +2135,17 @@ export class CommandCenter { quickpick.items = picks; quickpick.busy = false; - const choice = await new Promise(c => quickpick.onDidAccept(() => c(quickpick.activeItems[0]))); + const choice = await new Promise(c => { + quickpick.onDidAccept(() => c(quickpick.activeItems[0])); + quickpick.onDidTriggerItemButton((e) => { + quickpick.hide(); + const button = e.button as QuickInputButton & { actual: RemoteSourceAction }; + const item = e.item as CheckoutItem; + if (button.actual && item.refName) { + button.actual.run(item.refRemote ? item.refName.substring(item.refRemote.length + 1) : item.refName); + } + }); + }); quickpick.hide(); if (!choice) { diff --git a/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index 7c8ee74043c..c630f00c712 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -131,7 +131,7 @@ class GitDecorationProvider implements FileDecorationProvider { bucket.set(r.rightUri.toString(), decoration); } - if (r.type === Status.INDEX_RENAMED) { + if (r.type === Status.INDEX_RENAMED || r.type === Status.INTENT_TO_RENAME) { bucket.set(r.resourceUri.toString(), decoration); } } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 1fe5a80dba4..8290560e3ce 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -15,7 +15,7 @@ import * as filetype from 'file-type'; import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals } from './util'; import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Progress, Uri, workspace } from 'vscode'; import { detectEncoding } from './encoding'; -import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery } from './api/git'; +import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery, InitOptions } from './api/git'; import * as byline from 'byline'; import { StringDecoder } from 'string_decoder'; @@ -401,7 +401,7 @@ export class Git { return new Repository(this, repository, dotGit, logger); } - async init(repository: string, options: { defaultBranch?: string } = {}): Promise { + async init(repository: string, options: InitOptions = {}): Promise { const args = ['init']; if (options.defaultBranch && options.defaultBranch !== '') { @@ -793,7 +793,7 @@ export class GitStatusParser { // space i++; - if (entry.x === 'R' || entry.x === 'C') { + if (entry.x === 'R' || entry.y === 'R' || entry.x === 'C') { lastIndex = raw.indexOf('\0', i); if (lastIndex === -1) { diff --git a/extensions/git/src/remoteSource.ts b/extensions/git/src/remoteSource.ts index 4f62181f00c..4fdd6f06c1d 100644 --- a/extensions/git/src/remoteSource.ts +++ b/extensions/git/src/remoteSource.ts @@ -11,3 +11,7 @@ export async function pickRemoteSource(options: PickRemoteSourceOptions & { bran export async function pickRemoteSource(options: PickRemoteSourceOptions = {}): Promise { return GitBaseApi.getAPI().pickRemoteSource(options); } + +export async function getRemoteSourceActions(url: string) { + return GitBaseApi.getAPI().getRemoteSourceActions(url); +} diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index e7ed1cad955..09f0355f9e7 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -58,6 +58,7 @@ export class Resource implements SourceControlResourceState { case Status.UNTRACKED: return l10n.t('Untracked'); case Status.IGNORED: return l10n.t('Ignored'); case Status.INTENT_TO_ADD: return l10n.t('Intent to Add'); + case Status.INTENT_TO_RENAME: return l10n.t('Intent to Rename'); case Status.BOTH_DELETED: return l10n.t('Conflict: Both Deleted'); case Status.ADDED_BY_US: return l10n.t('Conflict: Added By Us'); case Status.DELETED_BY_THEM: return l10n.t('Conflict: Deleted By Them'); @@ -71,7 +72,7 @@ export class Resource implements SourceControlResourceState { @memoize get resourceUri(): Uri { - if (this.renameResourceUri && (this._type === Status.MODIFIED || this._type === Status.DELETED || this._type === Status.INDEX_RENAMED || this._type === Status.INDEX_COPIED)) { + if (this.renameResourceUri && (this._type === Status.MODIFIED || this._type === Status.DELETED || this._type === Status.INDEX_RENAMED || this._type === Status.INDEX_COPIED || this._type === Status.INTENT_TO_RENAME)) { return this.renameResourceUri; } @@ -136,6 +137,7 @@ export class Resource implements SourceControlResourceState { case Status.UNTRACKED: return Resource.Icons[theme].Untracked; case Status.IGNORED: return Resource.Icons[theme].Ignored; case Status.INTENT_TO_ADD: return Resource.Icons[theme].Added; + case Status.INTENT_TO_RENAME: return Resource.Icons[theme].Renamed; case Status.BOTH_DELETED: return Resource.Icons[theme].Conflict; case Status.ADDED_BY_US: return Resource.Icons[theme].Conflict; case Status.DELETED_BY_THEM: return Resource.Icons[theme].Conflict; @@ -193,6 +195,7 @@ export class Resource implements SourceControlResourceState { case Status.DELETED: return 'D'; case Status.INDEX_RENAMED: + case Status.INTENT_TO_RENAME: return 'R'; case Status.UNTRACKED: return 'U'; @@ -230,6 +233,7 @@ export class Resource implements SourceControlResourceState { return new ThemeColor('gitDecoration.addedResourceForeground'); case Status.INDEX_COPIED: case Status.INDEX_RENAMED: + case Status.INTENT_TO_RENAME: return new ThemeColor('gitDecoration.renamedResourceForeground'); case Status.UNTRACKED: return new ThemeColor('gitDecoration.untrackedResourceForeground'); @@ -520,6 +524,7 @@ class ResourceCommandResolver { case Status.INDEX_MODIFIED: case Status.INDEX_RENAMED: case Status.INDEX_ADDED: + case Status.INTENT_TO_RENAME: return toGitUri(resource.original, 'HEAD'); case Status.MODIFIED: @@ -554,7 +559,8 @@ class ResourceCommandResolver { case Status.MODIFIED: case Status.UNTRACKED: case Status.IGNORED: - case Status.INTENT_TO_ADD: { + case Status.INTENT_TO_ADD: + case Status.INTENT_TO_RENAME: { const uriString = resource.resourceUri.toString(); const [indexStatus] = this.repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString); @@ -599,6 +605,10 @@ class ResourceCommandResolver { case Status.UNTRACKED: return l10n.t('{0} (Untracked)', basename); + case Status.INTENT_TO_ADD: + case Status.INTENT_TO_RENAME: + return l10n.t('{0} (Intent to add)', basename); + default: return ''; } @@ -2177,6 +2187,7 @@ export class Repository implements Disposable { case 'M': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.MODIFIED, useIcons, renameUri)); break; case 'D': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.DELETED, useIcons, renameUri)); break; case 'A': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.INTENT_TO_ADD, useIcons, renameUri)); break; + case 'R': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.INTENT_TO_RENAME, useIcons, renameUri)); break; } return undefined; diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 8a653e523c4..11b1d3d649a 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -7,11 +7,7 @@ import * as vscode from 'vscode'; import { API as GitAPI } from './typings/git'; import { publishRepository } from './publish'; import { DisposableStore } from './util'; -import { LinkContext, getLink } from './links'; - -function getVscodeDevHost(): string { - return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`; -} +import { LinkContext, getLink, getVscodeDevHost } from './links'; async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean, context: LinkContext, includeRange = true) { try { diff --git a/extensions/github/src/links.ts b/extensions/github/src/links.ts index 223da3712fe..97fb3218a2a 100644 --- a/extensions/github/src/links.ts +++ b/extensions/github/src/links.ts @@ -168,3 +168,17 @@ export function getLink(gitAPI: GitAPI, useSelection: boolean, hostPrefix?: stri return `${hostPrefix}/${repo.owner}/${repo.repo}${blobSegment }${fileSegments}`; } + +export function getBranchLink(url: string, branch: string, hostPrefix: string = 'https://github.com') { + const repo = getRepositoryFromUrl(url); + if (!repo) { + throw new Error('Invalid repository URL provided'); + } + + branch = encodeURIComponentExceptSlashes(branch); + return `${hostPrefix}/${repo.owner}/${repo.repo}/tree/${branch}`; +} + +export function getVscodeDevHost(): string { + return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`; +} diff --git a/extensions/github/src/publish.ts b/extensions/github/src/publish.ts index 3f24b1061cc..dee8898d348 100644 --- a/extensions/github/src/publish.ts +++ b/extensions/github/src/publish.ts @@ -190,7 +190,7 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository) progress.report({ message: vscode.l10n.t('Creating first commit'), increment: 25 }); if (!repository) { - repository = await gitAPI.init(folder) || undefined; + repository = await gitAPI.init(folder, { defaultBranch: createdGithubRepository.default_branch }) || undefined; if (!repository) { return; diff --git a/extensions/github/src/remoteSourceProvider.ts b/extensions/github/src/remoteSourceProvider.ts index e8eeb851549..0d8b9340695 100644 --- a/extensions/github/src/remoteSourceProvider.ts +++ b/extensions/github/src/remoteSourceProvider.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workspace } from 'vscode'; -import { RemoteSourceProvider, RemoteSource } from './typings/git-base'; +import { Uri, env, l10n, workspace } from 'vscode'; +import { RemoteSourceProvider, RemoteSource, RemoteSourceAction } from './typings/git-base'; import { getOctokit } from './auth'; import { Octokit } from '@octokit/rest'; import { getRepositoryFromQuery, getRepositoryFromUrl } from './util'; +import { getBranchLink, getVscodeDevHost } from './links'; function asRemoteSource(raw: any): RemoteSource { const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol'); @@ -112,4 +113,27 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider { return branches.sort((a, b) => a === defaultBranch ? -1 : b === defaultBranch ? 1 : 0); } + + async getRemoteSourceActions(url: string): Promise { + const repository = getRepositoryFromUrl(url); + if (!repository) { + return []; + } + + return [{ + label: l10n.t('Open on GitHub'), + icon: 'github', + run(branch: string) { + const link = getBranchLink(url, branch); + env.openExternal(Uri.parse(link)); + } + }, { + label: l10n.t('Checkout on vscode.dev'), + icon: 'globe', + run(branch: string) { + const link = getBranchLink(url, branch, getVscodeDevHost()); + env.openExternal(Uri.parse(link)); + } + }]; + } } diff --git a/extensions/github/src/typings/git-base.d.ts b/extensions/github/src/typings/git-base.d.ts index 8510df6d043..53cac4d5c70 100644 --- a/extensions/github/src/typings/git-base.d.ts +++ b/extensions/github/src/typings/git-base.d.ts @@ -44,6 +44,15 @@ export interface PickRemoteSourceResult { readonly branch?: string; } +export interface RemoteSourceAction { + readonly label: string; + /** + * Codicon name + */ + readonly icon: string; + run(branch: string): void; +} + export interface RemoteSource { readonly name: string; readonly description?: string; @@ -70,6 +79,7 @@ export interface RemoteSourceProvider { readonly supportsQuery?: boolean; getBranches?(url: string): ProviderResult; + getRemoteSourceActions?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } diff --git a/extensions/github/src/typings/git.d.ts b/extensions/github/src/typings/git.d.ts index 83b00f324c0..4b4acd66879 100644 --- a/extensions/github/src/typings/git.d.ts +++ b/extensions/github/src/typings/git.d.ts @@ -78,6 +78,7 @@ export const enum Status { UNTRACKED, IGNORED, INTENT_TO_ADD, + INTENT_TO_RENAME, ADDED_BY_US, ADDED_BY_THEM, @@ -156,6 +157,10 @@ export interface FetchOptions { depth?: number; } +export interface InitOptions { + defaultBranch?: string; +} + export interface BranchQuery { readonly remote?: boolean; readonly pattern?: string; @@ -301,7 +306,7 @@ export interface API { toGitUri(uri: Uri, ref: string): Uri; getRepository(uri: Uri): Repository | null; - init(root: Uri): Promise; + init(root: Uri, options?: InitOptions): Promise; openRepository(root: Uri): Promise registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable; diff --git a/extensions/json/build/update-grammars.js b/extensions/json/build/update-grammars.js index 3fb40c86623..ff00951e126 100644 --- a/extensions/json/build/update-grammars.js +++ b/extensions/json/build/update-grammars.js @@ -6,8 +6,8 @@ var updateGrammar = require('vscode-grammar-updater'); -function adaptJSON(grammar, replacementScope) { - grammar.name = 'JSON with comments'; +function adaptJSON(grammar, name, replacementScope) { + grammar.name = name; grammar.scopeName = `source${replacementScope}`; var fixScopeNames = function (rule) { @@ -33,9 +33,5 @@ function adaptJSON(grammar, replacementScope) { var tsGrammarRepo = 'microsoft/vscode-JSON.tmLanguage'; updateGrammar.update(tsGrammarRepo, 'JSON.tmLanguage', './syntaxes/JSON.tmLanguage.json'); -updateGrammar.update(tsGrammarRepo, 'JSON.tmLanguage', './syntaxes/JSONC.tmLanguage.json', grammar => adaptJSON(grammar, '.json.comments')); - - - - - +updateGrammar.update(tsGrammarRepo, 'JSON.tmLanguage', './syntaxes/JSONC.tmLanguage.json', grammar => adaptJSON(grammar, 'JSON with Comments', '.json.comments')); +updateGrammar.update(tsGrammarRepo, 'JSON.tmLanguage', './syntaxes/JSONL.tmLanguage.json', grammar => adaptJSON(grammar, 'JSON Lines', '.json.lines')); diff --git a/extensions/json/package.json b/extensions/json/package.json index ba3b94b8c78..606e989c4d6 100644 --- a/extensions/json/package.json +++ b/extensions/json/package.json @@ -31,7 +31,7 @@ ".jslintrc", ".jsonld", ".geojson", - ".ipynb" + ".ipynb" ], "filenames": [ "composer.lock", @@ -65,6 +65,17 @@ "typedoc.json" ], "configuration": "./language-configuration.json" + }, + { + "id": "jsonl", + "aliases": [ + "JSON Lines" + ], + "extensions": [ + ".jsonl" + ], + "filenames": [], + "configuration": "./language-configuration.json" } ], "grammars": [ @@ -77,6 +88,11 @@ "language": "jsonc", "scopeName": "source.json.comments", "path": "./syntaxes/JSONC.tmLanguage.json" + }, + { + "language": "jsonl", + "scopeName": "source.json.lines", + "path": "./syntaxes/JSONL.tmLanguage.json" } ] }, diff --git a/extensions/json/syntaxes/JSONC.tmLanguage.json b/extensions/json/syntaxes/JSONC.tmLanguage.json index 31828ba65bb..ae5430630f6 100644 --- a/extensions/json/syntaxes/JSONC.tmLanguage.json +++ b/extensions/json/syntaxes/JSONC.tmLanguage.json @@ -5,7 +5,7 @@ "Once accepted there, we are happy to receive an update request." ], "version": "https://github.com/microsoft/vscode-JSON.tmLanguage/commit/9bd83f1c252b375e957203f21793316203f61f70", - "name": "JSON with comments", + "name": "JSON with Comments", "scopeName": "source.json.comments", "patterns": [ { diff --git a/extensions/json/syntaxes/JSONL.tmLanguage.json b/extensions/json/syntaxes/JSONL.tmLanguage.json new file mode 100644 index 00000000000..26de8d856f8 --- /dev/null +++ b/extensions/json/syntaxes/JSONL.tmLanguage.json @@ -0,0 +1,213 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/microsoft/vscode-JSON.tmLanguage/blob/master/JSON.tmLanguage", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/microsoft/vscode-JSON.tmLanguage/commit/9bd83f1c252b375e957203f21793316203f61f70", + "name": "JSON Lines", + "scopeName": "source.json.lines", + "patterns": [ + { + "include": "#value" + } + ], + "repository": { + "array": { + "begin": "\\[", + "beginCaptures": { + "0": { + "name": "punctuation.definition.array.begin.json.lines" + } + }, + "end": "\\]", + "endCaptures": { + "0": { + "name": "punctuation.definition.array.end.json.lines" + } + }, + "name": "meta.structure.array.json.lines", + "patterns": [ + { + "include": "#value" + }, + { + "match": ",", + "name": "punctuation.separator.array.json.lines" + }, + { + "match": "[^\\s\\]]", + "name": "invalid.illegal.expected-array-separator.json.lines" + } + ] + }, + "comments": { + "patterns": [ + { + "begin": "/\\*\\*(?!/)", + "captures": { + "0": { + "name": "punctuation.definition.comment.json.lines" + } + }, + "end": "\\*/", + "name": "comment.block.documentation.json.lines" + }, + { + "begin": "/\\*", + "captures": { + "0": { + "name": "punctuation.definition.comment.json.lines" + } + }, + "end": "\\*/", + "name": "comment.block.json.lines" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.comment.json.lines" + } + }, + "match": "(//).*$\\n?", + "name": "comment.line.double-slash.js" + } + ] + }, + "constant": { + "match": "\\b(?:true|false|null)\\b", + "name": "constant.language.json.lines" + }, + "number": { + "match": "(?x) # turn on extended mode\n -? # an optional minus\n (?:\n 0 # a zero\n | # ...or...\n [1-9] # a 1-9 character\n \\d* # followed by zero or more digits\n )\n (?:\n (?:\n \\. # a period\n \\d+ # followed by one or more digits\n )?\n (?:\n [eE] # an e character\n [+-]? # followed by an option +/-\n \\d+ # followed by one or more digits\n )? # make exponent optional\n )? # make decimal portion optional", + "name": "constant.numeric.json.lines" + }, + "object": { + "begin": "\\{", + "beginCaptures": { + "0": { + "name": "punctuation.definition.dictionary.begin.json.lines" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.dictionary.end.json.lines" + } + }, + "name": "meta.structure.dictionary.json.lines", + "patterns": [ + { + "comment": "the JSON object key", + "include": "#objectkey" + }, + { + "include": "#comments" + }, + { + "begin": ":", + "beginCaptures": { + "0": { + "name": "punctuation.separator.dictionary.key-value.json.lines" + } + }, + "end": "(,)|(?=\\})", + "endCaptures": { + "1": { + "name": "punctuation.separator.dictionary.pair.json.lines" + } + }, + "name": "meta.structure.dictionary.value.json.lines", + "patterns": [ + { + "comment": "the JSON object value", + "include": "#value" + }, + { + "match": "[^\\s,]", + "name": "invalid.illegal.expected-dictionary-separator.json.lines" + } + ] + }, + { + "match": "[^\\s\\}]", + "name": "invalid.illegal.expected-dictionary-separator.json.lines" + } + ] + }, + "string": { + "begin": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.json.lines" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.json.lines" + } + }, + "name": "string.quoted.double.json.lines", + "patterns": [ + { + "include": "#stringcontent" + } + ] + }, + "objectkey": { + "begin": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.support.type.property-name.begin.json.lines" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.support.type.property-name.end.json.lines" + } + }, + "name": "string.json.lines support.type.property-name.json.lines", + "patterns": [ + { + "include": "#stringcontent" + } + ] + }, + "stringcontent": { + "patterns": [ + { + "match": "(?x) # turn on extended mode\n \\\\ # a literal backslash\n (?: # ...followed by...\n [\"\\\\/bfnrt] # one of these characters\n | # ...or...\n u # a u\n [0-9a-fA-F]{4}) # and four hex digits", + "name": "constant.character.escape.json.lines" + }, + { + "match": "\\\\.", + "name": "invalid.illegal.unrecognized-string-escape.json.lines" + } + ] + }, + "value": { + "patterns": [ + { + "include": "#constant" + }, + { + "include": "#number" + }, + { + "include": "#string" + }, + { + "include": "#array" + }, + { + "include": "#object" + }, + { + "include": "#comments" + } + ] + } + } +} \ No newline at end of file diff --git a/extensions/markdown-language-features/server/package.json b/extensions/markdown-language-features/server/package.json index 6a101879701..55fc5fdd847 100644 --- a/extensions/markdown-language-features/server/package.json +++ b/extensions/markdown-language-features/server/package.json @@ -1,7 +1,7 @@ { "name": "vscode-markdown-languageserver", "description": "Markdown language server", - "version": "0.4.0-alpha.1", + "version": "0.4.0-alpha.2", "author": "Microsoft Corporation", "license": "MIT", "engines": { @@ -18,7 +18,7 @@ "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", - "vscode-markdown-languageservice": "^0.4.0-alpha.1", + "vscode-markdown-languageservice": "^0.4.0-alpha.2", "vscode-uri": "^3.0.7" }, "devDependencies": { diff --git a/extensions/markdown-language-features/server/yarn.lock b/extensions/markdown-language-features/server/yarn.lock index 1a47c2a1746..b801eb22ebc 100644 --- a/extensions/markdown-language-features/server/yarn.lock +++ b/extensions/markdown-language-features/server/yarn.lock @@ -128,10 +128,10 @@ vscode-languageserver@^8.1.0: dependencies: vscode-languageserver-protocol "3.17.3" -vscode-markdown-languageservice@^0.4.0-alpha.1: - version "0.4.0-alpha.1" - resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.4.0-alpha.1.tgz#367582d711a95001adb1cfae2eb8c9852cc51319" - integrity sha512-MqNHwKaO5UliiJ2lNSt8FRj/68max/7/N4JwD0mLkx/NAWLXYLluBbE1CQh/Fgycpcueldk0cckKYj/qNWDtVA== +vscode-markdown-languageservice@^0.4.0-alpha.2: + version "0.4.0-alpha.2" + resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.4.0-alpha.2.tgz#2edbd157ada35922ec762a7d6550d87a0b78959a" + integrity sha512-m2x+3dezndpDqfviCzsfUgAySVhoN8266OnasPpPlIZIho3a/JcUmFo6GZDlWBtOQXd9FT+TSAC2BPDCtWlhPQ== dependencies: "@vscode/l10n" "^0.0.10" node-html-parser "^6.1.5" diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts index 3892941b7ce..c0ecbe9a850 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts @@ -14,6 +14,7 @@ import { getDocumentDir } from '../../util/document'; enum MediaKind { Image, Video, + Audio, } export const mediaFileExtensions = new Map([ @@ -35,6 +36,11 @@ export const mediaFileExtensions = new Map([ // Videos ['ogg', MediaKind.Video], ['mp4', MediaKind.Video], + + // Audio Files + ['mp3', MediaKind.Audio], + ['aac', MediaKind.Audio], + ['wav', MediaKind.Audio], ]); export const mediaMimes = new Set([ @@ -45,6 +51,9 @@ export const mediaMimes = new Set([ 'image/webp', 'video/mp4', 'video/ogg', + 'audio/mpeg', + 'audio/aac', + 'audio/x-wav', ]); @@ -97,6 +106,7 @@ export function createUriListSnippet( let insertedLinkCount = 0; let insertedImageCount = 0; + let insertedAudioVideoCount = 0; uris.forEach((uri, i) => { const mdPath = getMdPath(dir, uri); @@ -104,12 +114,18 @@ export function createUriListSnippet( const ext = URI.Utils.extname(uri).toLowerCase().replace('.', ''); const insertAsMedia = typeof options?.insertAsMedia === 'undefined' ? mediaFileExtensions.has(ext) : !!options.insertAsMedia; const insertAsVideo = mediaFileExtensions.get(ext) === MediaKind.Video; + const insertAsAudio = mediaFileExtensions.get(ext) === MediaKind.Audio; if (insertAsVideo) { - insertedImageCount++; + insertedAudioVideoCount++; snippet.appendText(`'); + } else if (insertAsAudio) { + insertedAudioVideoCount++; + snippet.appendText(`'); } else { if (insertAsMedia) { insertedImageCount++; @@ -132,7 +148,13 @@ export function createUriListSnippet( }); let label: string; - if (insertedImageCount > 0 && insertedLinkCount > 0) { + if (insertedAudioVideoCount > 0) { + if (insertedLinkCount > 0) { + label = vscode.l10n.t('Insert Markdown Media and Links'); + } else { + label = vscode.l10n.t('Insert Markdown Media'); + } + } else if (insertedImageCount > 0 && insertedLinkCount > 0) { label = vscode.l10n.t('Insert Markdown Images and Links'); } else if (insertedImageCount > 0) { label = insertedImageCount > 1 diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index 10fd6c99eb2..0f095e20a2b 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -265,6 +265,11 @@ function scrollingEnabled(output: OutputItem, options: RenderOptions) { metadata.scrollable : options.outputScrolling; } +// div.cell_container +// div.output_container +// div.output.output-stream <-- outputElement parameter +// div.scrollable? tabindex="0" <-- contentParent +// div output-item-id="{guid}" <-- content from outputItem parameter function renderStream(outputInfo: OutputItem, outputElement: HTMLElement, error: boolean, ctx: IRichRenderContext): IDisposable { const disposableStore = createDisposableStore(); const outputScrolling = scrollingEnabled(outputInfo, ctx.settings); @@ -272,39 +277,50 @@ function renderStream(outputInfo: OutputItem, outputElement: HTMLElement, error: outputElement.classList.add('output-stream'); const text = outputInfo.text(); - const content = createOutputContent(outputInfo.id, [text], ctx.settings.lineLimit, outputScrolling, false); - content.setAttribute('output-item-id', outputInfo.id); + const newContent = createOutputContent(outputInfo.id, [text], ctx.settings.lineLimit, outputScrolling, false); + newContent.setAttribute('output-item-id', outputInfo.id); if (error) { - content.classList.add('error'); + newContent.classList.add('error'); } const scrollTop = outputScrolling ? findScrolledHeight(outputElement) : undefined; + const existingContent = outputElement.querySelector(`[output-item-id="${outputInfo.id}"]`) as HTMLElement | null; + const previousOutputParent = getPreviousMatchingContentGroup(outputElement); + // If the previous output item for the same cell was also a stream, append this output to the previous - const existingContentParent = getPreviousMatchingContentGroup(outputElement); - if (existingContentParent) { - const existing = existingContentParent.querySelector(`[output-item-id="${outputInfo.id}"]`) as HTMLElement | null; - if (existing) { - existing.replaceWith(content); + if (previousOutputParent) { + if (existingContent) { + existingContent.replaceWith(newContent); + } else { - existingContentParent.appendChild(content); + previousOutputParent.appendChild(newContent); } - existingContentParent.classList.toggle('scrollbar-visible', existingContentParent.scrollHeight > existingContentParent.clientHeight); - existingContentParent.scrollTop = scrollTop !== undefined ? scrollTop : existingContentParent.scrollHeight; + previousOutputParent.classList.toggle('scrollbar-visible', previousOutputParent.scrollHeight > previousOutputParent.clientHeight); + previousOutputParent.scrollTop = scrollTop !== undefined ? scrollTop : previousOutputParent.scrollHeight; } else { - const contentParent = document.createElement('div'); - contentParent.appendChild(content); + let contentParent = existingContent?.parentElement; + if (existingContent && contentParent) { + existingContent.replaceWith(newContent); + while (newContent.nextSibling) { + // clear out any stale content if we had previously combined streaming outputs into this one + newContent.nextSibling.remove(); + } + } else { + contentParent = document.createElement('div'); + contentParent.appendChild(newContent); + while (outputElement.firstChild) { + outputElement.removeChild(outputElement.firstChild); + } + outputElement.appendChild(contentParent); + } + contentParent.classList.toggle('scrollable', outputScrolling); contentParent.classList.toggle('word-wrap', ctx.settings.outputWordWrap); disposableStore.push(ctx.onDidChangeSettings(e => { - contentParent.classList.toggle('word-wrap', e.outputWordWrap); + contentParent!.classList.toggle('word-wrap', e.outputWordWrap); })); - - while (outputElement.firstChild) { - outputElement.removeChild(outputElement.firstChild); - } - outputElement.appendChild(contentParent); initializeScroll(contentParent, disposableStore, scrollTop); } diff --git a/extensions/notebook-renderers/src/test/notebookRenderer.test.ts b/extensions/notebook-renderers/src/test/notebookRenderer.test.ts index 3ce2c4019e8..340fff17c8a 100644 --- a/extensions/notebook-renderers/src/test/notebookRenderer.test.ts +++ b/extensions/notebook-renderers/src/test/notebookRenderer.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { activate } from '..'; import { OutputItem, RendererApi } from 'vscode-notebook-renderer'; -import { IRichRenderContext, RenderOptions } from '../rendererTypes'; +import { IDisposable, IRichRenderContext, RenderOptions } from '../rendererTypes'; import { JSDOM } from "jsdom"; const dom = new JSDOM(); @@ -37,7 +37,15 @@ suite('Notebook builtin output renderer', () => { type optionalRenderOptions = { [k in keyof RenderOptions]?: RenderOptions[k] }; + type handler = (e: RenderOptions) => any; + + const settingsChangedHandlers: handler[] = []; + function fireSettingsChange(options: optionalRenderOptions) { + settingsChangedHandlers.forEach((handler) => handler(options as RenderOptions)); + } + function createContext(settings?: optionalRenderOptions): IRichRenderContext { + settingsChangedHandlers.length = 0; return { setState(_value: void) { }, getState() { return undefined; }, @@ -48,9 +56,16 @@ suite('Notebook builtin output renderer', () => { lineLimit: 30, ...settings } as RenderOptions, - onDidChangeSettings(_listener: (e: T) => any, _thisArgs?: any, _disposables?: any) { + onDidChangeSettings(listener: handler, _thisArgs?: any, disposables?: IDisposable[]) { + settingsChangedHandlers.push(listener); + + const dispose = () => { + settingsChangedHandlers.splice(settingsChangedHandlers.indexOf(listener), 1); + }; + + disposables?.push({ dispose }); return { - dispose(): void { } + dispose }; }, workspace: { @@ -146,7 +161,7 @@ suite('Notebook builtin output renderer', () => { const inserted = outputElement.firstChild as HTMLElement; assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`); - assert.ok(!outputElement.classList.contains('remove-padding'), `Padding should not be removed for non-scrollable outputs: ${outputElement.classList}`); + assert.ok(outputElement.classList.contains('remove-padding'), `Padding should be removed for non-scrollable outputs: ${outputElement.classList}`); assert.ok(!inserted.classList.contains('word-wrap') && !inserted.classList.contains('scrollable'), `output content classList should not contain word-wrap and scrollable ${inserted.classList}`); assert.ok(inserted.innerHTML.indexOf('>content -1, `Content was not added to output element: ${outputElement.innerHTML}`); @@ -225,5 +240,58 @@ suite('Notebook builtin output renderer', () => { assert.ok(inserted.innerHTML.indexOf('>second stream content -1, `Content was not added to output element: ${outputElement.innerHTML}`); assert.ok(inserted.innerHTML.indexOf('>third stream content -1, `Content was not added to output element: ${outputElement.innerHTML}`); }); + + test(`Multiple adjacent streaming outputs, rerendering the first should erase the rest`, async () => { + const context = createContext(); + const renderer = await activate(context); + assert.ok(renderer, 'Renderer not created'); + + const outputHtml = new OutputHtml(); + const outputElement = outputHtml.getFirstOuputElement(); + const outputItem1 = createOutputItem('first stream content', stdoutMimeType, '1'); + const outputItem2 = createOutputItem('second stream content', stdoutMimeType, '2'); + const outputItem3 = createOutputItem('third stream content', stderrMimeType, '3'); + await renderer!.renderOutputItem(outputItem1, outputElement); + await renderer!.renderOutputItem(outputItem2, outputHtml.appendOutputElement()); + await renderer!.renderOutputItem(outputItem3, outputHtml.appendOutputElement()); + const newOutputItem1 = createOutputItem('replaced content', stderrMimeType, '1'); + await renderer!.renderOutputItem(newOutputItem1, outputElement); + + + const inserted = outputElement.firstChild as HTMLElement; + assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`); + assert.ok(inserted.innerHTML.indexOf('>replaced content -1, `Content was not added to output element: ${outputElement.innerHTML}`); + assert.ok(inserted.innerHTML.indexOf('>first stream contentsecond stream contentthird stream content { + const context = createContext({ outputWordWrap: false, outputScrolling: true }); + const renderer = await activate(context); + assert.ok(renderer, 'Renderer not created'); + + const outputElement = new OutputHtml().getFirstOuputElement(); + const outputItem = createOutputItem('content', stdoutMimeType); + await renderer!.renderOutputItem(outputItem, outputElement); + fireSettingsChange({ outputWordWrap: true, outputScrolling: true }); + + const inserted = outputElement.firstChild as HTMLElement; + assert.ok(inserted.classList.contains('word-wrap') && inserted.classList.contains('scrollable'), + `output content classList should contain word-wrap and scrollable ${inserted.classList}`); + }); + + test(`Settings event change listeners should not grow if output is re-rendered`, async () => { + const context = createContext({ outputWordWrap: false }); + const renderer = await activate(context); + assert.ok(renderer, 'Renderer not created'); + + const outputElement = new OutputHtml().getFirstOuputElement(); + await renderer!.renderOutputItem(createOutputItem('content', stdoutMimeType), outputElement); + const handlerCount = settingsChangedHandlers.length; + await renderer!.renderOutputItem(createOutputItem('content', stdoutMimeType), outputElement); + + assert.equal(settingsChangedHandlers.length, handlerCount); + }); }); diff --git a/extensions/theme-seti/build/update-icon-theme.js b/extensions/theme-seti/build/update-icon-theme.js index ab2a1c81e81..366e7f37dd6 100644 --- a/extensions/theme-seti/build/update-icon-theme.js +++ b/extensions/theme-seti/build/update-icon-theme.js @@ -44,6 +44,7 @@ const nonBuiltInLanguages = { // { fileNames, extensions } // list of languagesId that inherit the icon from another language const inheritIconFromLanguage = { "jsonc": 'json', + "jsonl": 'json', "postcss": 'css', "django-html": 'html', "blade": 'php' diff --git a/extensions/theme-seti/icons/vs-seti-icon-theme.json b/extensions/theme-seti/icons/vs-seti-icon-theme.json index f5b78299680..184cded8cad 100644 --- a/extensions/theme-seti/icons/vs-seti-icon-theme.json +++ b/extensions/theme-seti/icons/vs-seti-icon-theme.json @@ -1943,6 +1943,7 @@ "todo": "_todo", "vala": "_vala", "vue": "_vue", + "jsonl": "_json", "postcss": "_css", "django-html": "_html_3", "blade": "_php" @@ -2257,6 +2258,7 @@ "terraform": "_terraform_light", "vala": "_vala_light", "vue": "_vue_light", + "jsonl": "_json_light", "postcss": "_css_light", "django-html": "_html_3_light", "blade": "_php_light" diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 0393fbb0ea4..26fd5e64d96 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -143,6 +143,12 @@ "title": "%configuration.typescript%", "order": 20, "properties": { + "typescript.experimental.aiQuickFix": { + "type": "boolean", + "default": false, + "description": "%typescript.experimental.aiQuickFix%", + "scope": "resource" + }, "typescript.tsdk": { "type": "string", "markdownDescription": "%typescript.tsdk.desc%", diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 2d5aa3e7eb7..c46952747fd 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -8,6 +8,7 @@ "configuration.suggest.completeFunctionCalls": "Complete functions with their parameter signature.", "configuration.suggest.includeAutomaticOptionalChainCompletions": "Enable/disable showing completions on potentially undefined values that insert an optional chain call. Requires strict null checks to be enabled.", "configuration.suggest.includeCompletionsForImportStatements": "Enable/disable auto-import-style completions on partially-typed import statements.", + "typescript.experimental.aiQuickFix": "Enable/disable AI-assisted quick fixes.", "typescript.tsdk.desc": "Specifies the folder path to the tsserver and `lib*.d.ts` files under a TypeScript install to use for IntelliSense, for example: `./node_modules/typescript/lib`.\n\n- When specified as a user setting, the TypeScript version from `typescript.tsdk` automatically replaces the built-in TypeScript version.\n- When specified as a workspace setting, `typescript.tsdk` allows you to switch to use that workspace version of TypeScript for IntelliSense with the `TypeScript: Select TypeScript version` command.\n\nSee the [TypeScript documentation](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-newer-typescript-versions) for more detail about managing TypeScript versions.", "typescript.disableAutomaticTypeAcquisition": "Disables [automatic type acquisition](https://code.visualstudio.com/docs/nodejs/working-with-javascript#_typings-and-automatic-type-acquisition). Automatic type acquisition fetches `@types` packages from npm to improve IntelliSense for external libraries.", "typescript.enablePromptUseWorkspaceTsdk": "Enables prompting of users to use the TypeScript version configured in the workspace for Intellisense.", diff --git a/extensions/typescript-language-features/src/languageFeatures/completions.ts b/extensions/typescript-language-features/src/languageFeatures/completions.ts index fcc5dc64c6f..8b5100f876e 100644 --- a/extensions/typescript-language-features/src/languageFeatures/completions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/completions.ts @@ -93,7 +93,6 @@ class MyCompletionItem extends vscode.CompletionItem { this.range = this.getRangeFromReplacementSpan(tsEntry, completionContext); this.commitCharacters = MyCompletionItem.getCommitCharacters(completionContext, tsEntry); this.insertText = isSnippet && tsEntry.insertText ? new vscode.SnippetString(tsEntry.insertText) : tsEntry.insertText; - // @ts-expect-error until 5.2 this.filterText = tsEntry.filterText || this.getFilterText(completionContext.line, tsEntry.insertText); if (completionContext.isMemberCompletion && completionContext.dotAccessorContext && !(this.insertText instanceof vscode.SnippetString)) { diff --git a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts index 515402f6e1e..a6553902cbe 100644 --- a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts +++ b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts @@ -20,11 +20,49 @@ import { applyCodeActionCommands, getEditForCodeAction } from './util/codeAction import { conditionalRegistration, requireSomeCapability } from './util/dependentRegistration'; type ApplyCodeActionCommand_args = { - readonly resource: vscode.Uri; + readonly document: vscode.TextDocument; readonly diagnostic: vscode.Diagnostic; readonly action: Proto.CodeFixAction; + readonly followupAction?: Command; }; +class EditorChatFollowUp implements Command { + + id: string = 'needsBetterName.editorChateFollowUp'; + + constructor(private readonly prompt: string, private readonly document: vscode.TextDocument, private readonly range: vscode.Range, private readonly client: ITypeScriptServiceClient) { + + } + + async execute() { + const findScopeEndLineFromNavTree = (startLine: number, navigationTree: Proto.NavigationTree[]): vscode.Range | undefined => { + for (const node of navigationTree) { + const range = typeConverters.Range.fromTextSpan(node.spans[0]); + if (startLine === range.start.line) { + return range; + } else if (startLine > range.start.line && startLine <= range.end.line && node.childItems) { + return findScopeEndLineFromNavTree(startLine, node.childItems); + } + } + return undefined; + }; + const filepath = this.client.toOpenTsFilePath(this.document); + if (!filepath) { + return; + } + const response = await this.client.execute('navtree', { file: filepath }, (new vscode.CancellationTokenSource()).token); + if (response.type !== 'response' || !response.body?.childItems) { + return; + } + const startLine = this.range.start.line; + const enclosingRange = findScopeEndLineFromNavTree(startLine, response.body.childItems); + if (!enclosingRange) { + return; + } + await vscode.commands.executeCommand('vscode.editorChat.start', { initialRange: enclosingRange, message: this.prompt, autoSend: true }); + } +} + class ApplyCodeActionCommand implements Command { public static readonly ID = '_typescript.applyCodeActionCommand'; public readonly id = ApplyCodeActionCommand.ID; @@ -35,7 +73,7 @@ class ApplyCodeActionCommand implements Command { private readonly telemetryReporter: TelemetryReporter, ) { } - public async execute({ resource, action, diagnostic }: ApplyCodeActionCommand_args): Promise { + public async execute({ document, action, diagnostic, followupAction }: ApplyCodeActionCommand_args): Promise { /* __GDPR__ "quickFix.execute" : { "owner": "mjbvz", @@ -49,8 +87,10 @@ class ApplyCodeActionCommand implements Command { fixName: action.fixName }); - this.diagnosticManager.deleteDiagnostic(resource, diagnostic); - return applyCodeActionCommands(this.client, action.commands, nulToken); + this.diagnosticManager.deleteDiagnostic(document.uri, diagnostic); + const codeActionResult = await applyCodeActionCommands(this.client, action.commands, nulToken); + await followupAction?.execute(); + return codeActionResult; } } @@ -313,22 +353,27 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider{ action: tsAction, diagnostic, resource }], + arguments: [{ action: tsAction, diagnostic, document, followupAction }], title: '' }; return codeAction; diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 63a3e7bf194..c0766007ad6 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -43,10 +43,13 @@ "tokenInformation", "treeItemCheckbox", "treeViewReveal", + "testInvalidateResults", "workspaceTrust", "telemetry", "windowActivity", - "interactiveUserActions" + "interactiveUserActions", + "envCollectionWorkspace", + "envCollectionOptions" ], "private": true, "activationEvents": [], diff --git a/package.json b/package.json index e3146a1e297..0a6957a0fa6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.79.0", - "distro": "ba0897486f9570f3f031bd9512490baf0d646762", + "distro": "7272f69cc607298dc986708a73bd978aa7c3af3d", "author": { "name": "Microsoft Corporation" }, @@ -204,7 +204,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.1.4", - "typescript": "^5.2.0-dev.20230516", + "typescript": "^5.2.0-dev.20230524", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "util": "^0.12.4", diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index 1c8e0831975..778f1deb9a6 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -119,6 +119,7 @@ 'xterm-addon-canvas': `${baseNodeModulesPath}/xterm-addon-canvas/lib/xterm-addon-canvas.js`, 'xterm-addon-image': `${baseNodeModulesPath}/xterm-addon-image/lib/xterm-addon-image.js`, 'xterm-addon-search': `${baseNodeModulesPath}/xterm-addon-search/lib/xterm-addon-search.js`, + 'xterm-addon-serialize': `${baseNodeModulesPath}/xterm-addon-serialize/lib/xterm-addon-serialize.js`, 'xterm-addon-unicode11': `${baseNodeModulesPath}/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`, 'xterm-addon-webgl': `${baseNodeModulesPath}/xterm-addon-webgl/lib/xterm-addon-webgl.js`, '@vscode/iconv-lite-umd': `${baseNodeModulesPath}/@vscode/iconv-lite-umd/lib/iconv-lite-umd.js`, diff --git a/src/vs/base/test/browser/dom.test.ts b/src/vs/base/test/browser/dom.test.ts index 87f5b2d4cab..dfb9f0a342b 100644 --- a/src/vs/base/test/browser/dom.test.ts +++ b/src/vs/base/test/browser/dom.test.ts @@ -18,6 +18,8 @@ suite('dom', () => { assert(!element.classList.contains('bar')); assert(!element.classList.contains('foo')); assert(!element.classList.contains('')); + + }); test('removeClass', () => { diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index f23a291a750..44263745539 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -15,7 +15,7 @@ import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHos import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService'; -import { IssueReporter } from './IssueReporterService'; +import { IssueReporter } from './issueReporterService'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { getSingletonServiceDescriptors } from 'vs/platform/instantiation/common/extensions'; import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; diff --git a/src/vs/code/electron-sandbox/issue/IssueReporterService.ts b/src/vs/code/electron-sandbox/issue/issueReporterService.ts similarity index 100% rename from src/vs/code/electron-sandbox/issue/IssueReporterService.ts rename to src/vs/code/electron-sandbox/issue/issueReporterService.ts diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 18e0c99f060..df156d0bbc7 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -183,7 +183,7 @@ export class MenuId { static readonly InlineSuggestionToolbar = new MenuId('InlineSuggestionToolbar'); static readonly ChatContext = new MenuId('ChatContext'); static readonly ChatCodeBlock = new MenuId('ChatCodeblock'); - static readonly ChatTitle = new MenuId('ChatTitle'); + static readonly ChatMessageTitle = new MenuId('ChatMessageTitle'); static readonly ChatExecute = new MenuId('ChatExecute'); /** diff --git a/src/vs/workbench/api/browser/mainThreadChat.ts b/src/vs/workbench/api/browser/mainThreadChat.ts index fc6e18b077d..807882e0867 100644 --- a/src/vs/workbench/api/browser/mainThreadChat.ts +++ b/src/vs/workbench/api/browser/mainThreadChat.ts @@ -8,10 +8,10 @@ import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; -import { ExtHostContext, ExtHostChatShape, IChatRequestDto, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostChatShape, ExtHostContext, IChatRequestDto, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { IChatProgress, IChatRequest, IChatResponse, IChat, IChatDynamicRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChat, IChatDynamicRequest, IChatProgress, IChatRequest, IChatResponse, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @extHostNamedCustomer(MainContext.MainThreadChat) @@ -134,6 +134,9 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { }, provideFollowups: (session, token) => { return this._proxy.$provideFollowups(handle, session.id, token); + }, + removeRequest: (session, requestId) => { + return this._proxy.$removeRequest(handle, session.id, requestId); } }); diff --git a/src/vs/workbench/api/browser/mainThreadShare.ts b/src/vs/workbench/api/browser/mainThreadShare.ts index 60f86f96ed0..c1ab121b007 100644 --- a/src/vs/workbench/api/browser/mainThreadShare.ts +++ b/src/vs/workbench/api/browser/mainThreadShare.ts @@ -24,11 +24,12 @@ export class MainThreadShare implements MainThreadShareShape { this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostShare); } - $registerShareProvider(handle: number, selector: IDocumentFilterDto[], id: string, label: string): void { + $registerShareProvider(handle: number, selector: IDocumentFilterDto[], id: string, label: string, priority: number): void { const provider: IShareProvider = { id, label, selector, + priority, provideShare: async (item: IShareableItem) => { return URI.revive(await this.proxy.$provideShare(handle, item, new CancellationTokenSource().token)); } diff --git a/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/src/vs/workbench/api/browser/mainThreadStatusBar.ts index 21166883d5a..faf4ca55bf8 100644 --- a/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -11,7 +11,7 @@ import { Command } from 'vs/editor/common/languages'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IExtensionStatusBarItemService } from 'vs/workbench/api/browser/statusBarExtensionPoint'; -import { StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { IStatusbarEntry, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; @extHostNamedCustomer(MainContext.MainThreadStatusBar) export class MainThreadStatusBar implements MainThreadStatusBarShape { @@ -27,17 +27,27 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { // once, at startup read existing items and send them over const entries: StatusBarItemDto[] = []; for (const [entryId, item] of statusbarService.getEntries()) { - entries.push({ + entries.push(asDto(entryId, item)); + } + + proxy.$acceptStaticEntries(entries); + + statusbarService.onDidChange(e => { + if (e.added) { + proxy.$acceptStaticEntries([asDto(e.added[0], e.added[1])]); + } + }); + + function asDto(entryId: string, item: { entry: IStatusbarEntry; alignment: StatusbarAlignment; priority: number }): StatusBarItemDto { + return { entryId, name: item.entry.name, text: item.entry.text, command: typeof item.entry.command === 'string' ? item.entry.command : undefined, priority: item.priority, alignLeft: item.alignment === StatusbarAlignment.LEFT - }); + }; } - - proxy.$acceptStaticEntries(entries); } dispose(): void { diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index 676816725fc..d9ea8c1e677 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -52,6 +52,18 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh })); } + /** + * @inheritdoc + */ + $markTestRetired(testId: string): void { + for (const result of this.resultService.results) { + // all non-live results are already entirely outdated + if (result instanceof LiveTestResult) { + result.markRetired(testId); + } + } + } + /** * @inheritdoc */ diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index 846d68c2d14..ab5fabffdc2 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { MainThreadTunnelServiceShape, MainContext, ExtHostContext, ExtHostTunnelServiceShape, CandidatePortSource, PortAttributesProviderSelector, TunnelDto } from 'vs/workbench/api/common/extHost.protocol'; +import { MainThreadTunnelServiceShape, MainContext, ExtHostContext, ExtHostTunnelServiceShape, CandidatePortSource, PortAttributesSelector, TunnelDto } from 'vs/workbench/api/common/extHost.protocol'; import { TunnelDtoConverter } from 'vs/workbench/api/common/extHostTunnelService'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { CandidatePort, IRemoteExplorerService, makeAddress, PORT_AUTO_FORWARD_SETTING, PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_OUTPUT, TunnelCloseReason, TunnelSource } from 'vs/workbench/services/remote/common/remoteExplorerService'; @@ -23,7 +23,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'v export class MainThreadTunnelService extends Disposable implements MainThreadTunnelServiceShape, PortAttributesProvider { private readonly _proxy: ExtHostTunnelServiceShape; private elevateionRetry: boolean = false; - private portsAttributesProviders: Map = new Map(); + private portsAttributesProviders: Map = new Map(); constructor( extHostContext: IExtHostContext, @@ -63,7 +63,7 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun } private _alreadyRegistered: boolean = false; - async $registerPortsAttributesProvider(selector: PortAttributesProviderSelector, providerHandle: number): Promise { + async $registerPortsAttributesProvider(selector: PortAttributesSelector, providerHandle: number): Promise { this.portsAttributesProviders.set(providerHandle, selector); if (!this._alreadyRegistered) { this.remoteExplorerService.tunnelModel.addAttributesProvider(this); @@ -85,9 +85,8 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun const selector = entry[1]; const portRange = selector.portRange; const portInRange = portRange ? ports.some(port => portRange[0] <= port && port < portRange[1]) : true; - const pidMatches = !selector.pid || (selector.pid === pid); const commandMatches = !selector.commandPattern || (commandLine && (commandLine.match(selector.commandPattern))); - return portInRange && pidMatches && commandMatches; + return portInRange && commandMatches; }).map(entry => entry[0]); if (appropriateHandles.length === 0) { diff --git a/src/vs/workbench/api/browser/statusBarExtensionPoint.ts b/src/vs/workbench/api/browser/statusBarExtensionPoint.ts index 9cf4e62872f..48f91f7ffbe 100644 --- a/src/vs/workbench/api/browser/statusBarExtensionPoint.ts +++ b/src/vs/workbench/api/browser/statusBarExtensionPoint.ts @@ -16,6 +16,7 @@ import { IAccessibilityInformation } from 'vs/platform/accessibility/common/acce import { IMarkdownString } from 'vs/base/common/htmlContent'; import { getCodiconAriaLabel } from 'vs/base/common/iconLabels'; import { hash } from 'vs/base/common/hash'; +import { Event, Emitter } from 'vs/base/common/event'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Iterable } from 'vs/base/common/iterator'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -27,16 +28,24 @@ import { asStatusBarItemIdentifier } from 'vs/workbench/api/common/extHostTypes' export const IExtensionStatusBarItemService = createDecorator('IExtensionStatusBarItemService'); export interface IExtensionStatusBarItemChangeEvent { - readonly added?: Readonly<{ entryId: string } & IStatusbarEntry>; + readonly added?: ExtensionStatusBarEntry; readonly removed?: string; } +export type ExtensionStatusBarEntry = [string, { + entry: IStatusbarEntry; + alignment: MainThreadStatusBarAlignment; + priority: number; +}]; + export interface IExtensionStatusBarItemService { readonly _serviceBrand: undefined; + onDidChange: Event; + setOrUpdateEntry(id: string, statusId: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): IDisposable; - getEntries(): Iterable<[string, { entry: IStatusbarEntry; alignment: MainThreadStatusBarAlignment; priority: number }]>; + getEntries(): Iterable; } @@ -46,8 +55,17 @@ class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { private readonly _entries: Map = new Map(); + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + constructor(@IStatusbarService private readonly _statusbarService: IStatusbarService) { } + dispose(): void { + this._entries.forEach(entry => entry.accessor.dispose()); + this._entries.clear(); + this._onDidChange.dispose(); + } + setOrUpdateEntry(entryId: string, id: string, extensionId: string | undefined, name: string, text: string, tooltip: IMarkdownString | string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignLeft: boolean, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): IDisposable { // if there are icons in the text use the tooltip for the aria label let ariaLabel: string; @@ -98,6 +116,8 @@ class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { priority }); + this._onDidChange.fire({ added: [entryId, { entry, alignment, priority }] }); + } else { // Otherwise update existingEntry.accessor.update(entry); @@ -109,6 +129,7 @@ class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { if (entry) { entry.accessor.dispose(); this._entries.delete(entryId); + this._onDidChange.fire({ removed: entryId }); } }); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e6f76b539d0..1f67e2a66dd 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -202,7 +202,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol)); const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService)); - const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInteractiveEditor, new ExtHostInteractiveEditor(rpcProtocol, extHostDocuments, extHostLogService)); + const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInteractiveEditor, new ExtHostInteractiveEditor(rpcProtocol, extHostCommands, extHostDocuments, extHostLogService)); const extHostChat = rpcProtocol.set(ExtHostContext.ExtHostChat, new ExtHostChat(rpcProtocol, extHostLogService)); const extHostSemanticSimilarity = rpcProtocol.set(ExtHostContext.ExtHostSemanticSimilarity, new ExtHostSemanticSimilarity(rpcProtocol)); const extHostIssueReporter = rpcProtocol.set(ExtHostContext.ExtHostIssueReporter, new ExtHostIssueReporter(rpcProtocol)); @@ -1077,7 +1077,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'tunnels'); return extHostTunnelService.onDidChangeTunnels(listener, thisArg, disposables); }, - registerPortAttributesProvider: (portSelector: { pid?: number; portRange?: [number, number]; commandPattern?: RegExp }, provider: vscode.PortAttributesProvider) => { + registerPortAttributesProvider: (portSelector: vscode.PortAttributesSelector, provider: vscode.PortAttributesProvider) => { checkProposedApiEnabled(extension, 'portsAttributes'); return extHostTunnelService.registerPortsAttributesProvider(portSelector, provider); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a689fa33976..a42f890530d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1174,6 +1174,7 @@ export interface ExtHostChatShape { $provideWelcomeMessage(handle: number, token: CancellationToken): Promise<(string | IChatReplyFollowup[])[] | undefined>; $provideFollowups(handle: number, sessionId: number, token: CancellationToken): Promise; $provideReply(handle: number, sessionId: number, request: IChatRequestDto, token: CancellationToken): Promise; + $removeRequest(handle: number, sessionId: number, requestId: string): void; $provideSlashCommands(handle: number, sessionId: number, token: CancellationToken): Promise; $releaseSession(sessionId: number): void; $onDidPerformUserAction(event: IChatUserActionEvent): Promise; @@ -1265,7 +1266,7 @@ export interface MainThreadSearchShape extends IDisposable { } export interface MainThreadShareShape extends IDisposable { - $registerShareProvider(handle: number, selector: IDocumentFilterDto[], id: string, label: string): void; + $registerShareProvider(handle: number, selector: IDocumentFilterDto[], id: string, label: string, priority: number): void; $unregisterShareProvider(handle: number): void; } @@ -1420,8 +1421,7 @@ export enum CandidatePortSource { Output = 2 } -export interface PortAttributesProviderSelector { - pid?: number; +export interface PortAttributesSelector { portRange?: [number, number]; commandPattern?: RegExp; } @@ -1435,7 +1435,7 @@ export interface MainThreadTunnelServiceShape extends IDisposable { $setCandidateFilter(): Promise; $onFoundNewCandidates(candidates: CandidatePort[]): Promise; $setCandidatePortSource(source: CandidatePortSource): Promise; - $registerPortsAttributesProvider(selector: PortAttributesProviderSelector, providerHandle: number): Promise; + $registerPortsAttributesProvider(selector: PortAttributesSelector, providerHandle: number): Promise; $unregisterPortsAttributesProvider(providerHandle: number): Promise; } @@ -2501,6 +2501,8 @@ export interface MainThreadTestingShape { $startedExtensionTestRun(req: ExtensionRunTestsRequest): void; /** Signals that an extension-provided test run finished. */ $finishedExtensionTestRun(runId: string): void; + /** Marks a test (or controller) as retired in all results. */ + $markTestRetired(testId: string): void; } // --- proxy identifiers diff --git a/src/vs/workbench/api/common/extHostChat.ts b/src/vs/workbench/api/common/extHostChat.ts index b4aada3a1a0..2a623150731 100644 --- a/src/vs/workbench/api/common/extHostChat.ts +++ b/src/vs/workbench/api/common/extHostChat.ts @@ -13,7 +13,7 @@ import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/exte import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostChatShape, IChatRequestDto, IChatResponseDto, IChatDto, IMainContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { IChatFollowup, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatProgress, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; import type * as vscode from 'vscode'; class ChatProviderWrapper { @@ -158,6 +158,24 @@ export class ExtHostChat implements ExtHostChatShape { return rawFollowups?.map(f => typeConvert.ChatFollowup.from(f)); } + $removeRequest(handle: number, sessionId: number, requestId: string): void { + const entry = this._chatProvider.get(handle); + if (!entry) { + return; + } + + const realSession = this._chatSessions.get(sessionId); + if (!realSession) { + return; + } + + if (!entry.provider.removeRequest) { + return; + } + + entry.provider.removeRequest(realSession, requestId); + } + async $provideReply(handle: number, sessionId: number, request: IChatRequestDto, token: CancellationToken): Promise { const entry = this._chatProvider.get(handle); if (!entry) { @@ -186,7 +204,8 @@ export class ExtHostChat implements ExtHostChatShape { firstProgress = stopWatch.elapsed(); } - this._proxy.$acceptResponseProgress(handle, sessionId, progress); + const vscodeProgress: IChatProgress = 'responseId' in progress ? { requestId: progress.responseId } : progress; + this._proxy.$acceptResponseProgress(handle, sessionId, vscodeProgress); } }; let result: vscode.InteractiveResponseForProgress | undefined | null; diff --git a/src/vs/workbench/api/common/extHostInteractiveEditor.ts b/src/vs/workbench/api/common/extHostInteractiveEditor.ts index 94ba77eeb02..90164a44aab 100644 --- a/src/vs/workbench/api/common/extHostInteractiveEditor.ts +++ b/src/vs/workbench/api/common/extHostInteractiveEditor.ts @@ -15,6 +15,8 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import type * as vscode from 'vscode'; +import { ApiCommand, ApiCommandArgument, ApiCommandResult, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { IRange } from 'vs/editor/common/core/range'; class ProviderWrapper { @@ -47,10 +49,40 @@ export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape { constructor( mainContext: IMainContext, + extHostCommands: ExtHostCommands, private readonly _documents: ExtHostDocuments, private readonly _logService: ILogService, ) { this._proxy = mainContext.getProxy(MainContext.MainThreadInteractiveEditor); + + type EditorChatApiArg = { + initialRange?: vscode.Range; + message?: string; + autoSend?: boolean; + }; + + type InteractiveEditorRunOptions = { + initialRange?: IRange; + message?: string; + autoSend?: boolean; + }; + + extHostCommands.registerApiCommand(new ApiCommand( + 'vscode.editorChat.start', 'interactiveEditor.start', 'Invoke a new editor chat session', + [new ApiCommandArgument('Run arguments', '', _v => true, v => { + + if (!v) { + return undefined; + } + + return { + initialRange: v.initialRange ? typeConvert.Range.from(v.initialRange) : undefined, + message: v.message, + autoSend: v.autoSend + }; + })], + ApiCommandResult.Void + )); } registerProvider(extension: Readonly, provider: vscode.InteractiveEditorSessionProvider): vscode.Disposable { diff --git a/src/vs/workbench/api/common/extHostShare.ts b/src/vs/workbench/api/common/extHostShare.ts index 15acba45c47..618fe17138f 100644 --- a/src/vs/workbench/api/common/extHostShare.ts +++ b/src/vs/workbench/api/common/extHostShare.ts @@ -32,7 +32,7 @@ export class ExtHostShare implements ExtHostShareShape { registerShareProvider(selector: vscode.DocumentSelector, provider: vscode.ShareProvider): vscode.Disposable { const handle = ExtHostShare.handlePool++; this.providers.set(handle, provider); - this.proxy.$registerShareProvider(handle, DocumentSelector.from(selector, this.uriTransformer), provider.id, provider.label); + this.proxy.$registerShareProvider(handle, DocumentSelector.from(selector, this.uriTransformer), provider.id, provider.label, provider.priority); return { dispose: () => { this.proxy.$unregisterShareProvider(handle); diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 1cb4c5890d1..6dabdb4f4df 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -25,7 +25,7 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { Promises } from 'vs/base/common/async'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { ViewColumn } from 'vs/workbench/api/common/extHostTypeConverters'; -import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable { @@ -898,6 +898,10 @@ class UnifiedEnvironmentVariableCollection { } getScopedEnvironmentVariableCollection(scope: vscode.EnvironmentVariableScope | undefined): IEnvironmentVariableCollection { + if (this._extension && scope) { + // TODO: This should be removed when the env var extension API(s) are stabilized + checkProposedApiEnabled(this._extension, 'envCollectionWorkspace'); + } const scopedCollectionKey = this.getScopeKey(scope); let scopedCollection = this.scopedCollections.get(scopedCollectionKey); if (!scopedCollection) { @@ -910,21 +914,21 @@ class UnifiedEnvironmentVariableCollection { replace(variable: string, value: string, options: vscode.EnvironmentVariableMutatorOptions | undefined, scope: vscode.EnvironmentVariableScope | undefined): void { if (this._extension && options) { - isProposedApiEnabled(this._extension, 'envCollectionOptions'); + checkProposedApiEnabled(this._extension, 'envCollectionOptions'); } this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Replace, options: options ?? {}, scope }); } append(variable: string, value: string, options: vscode.EnvironmentVariableMutatorOptions | undefined, scope: vscode.EnvironmentVariableScope | undefined): void { if (this._extension && options) { - isProposedApiEnabled(this._extension, 'envCollectionOptions'); + checkProposedApiEnabled(this._extension, 'envCollectionOptions'); } this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Append, options: options ?? {}, scope }); } prepend(variable: string, value: string, options: vscode.EnvironmentVariableMutatorOptions | undefined, scope: vscode.EnvironmentVariableScope | undefined): void { if (this._extension && options) { - isProposedApiEnabled(this._extension, 'envCollectionOptions'); + checkProposedApiEnabled(this._extension, 'envCollectionOptions'); } this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Prepend, options: options ?? {}, scope }); } diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index ab1ffe90cea..5e2e6f1bc19 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -27,6 +27,7 @@ import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestItem, ITestItemContext, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; interface ControllerInfo { @@ -137,6 +138,11 @@ export class ExtHostTesting implements ExtHostTestingShape { createTestRun: (request, name, persist = true) => { return this.runTracker.createTestRun(controllerId, collection, request, name, persist); }, + invalidateTestResults: item => { + checkProposedApiEnabled(extension, 'testInvalidateResults'); + const id = item ? TestId.fromExtHostTestItem(item, controllerId).toString() : controllerId; + return this.proxy.$markTestRetired(id); + }, set resolveHandler(fn) { collection.resolveHandler = fn; }, diff --git a/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index a6ad6135611..dfdfc5ab5a2 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/src/vs/workbench/api/common/extHostTunnelService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtHostTunnelServiceShape, TunnelDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostTunnelServiceShape, PortAttributesSelector, TunnelDto } from 'vs/workbench/api/common/extHost.protocol'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import * as vscode from 'vscode'; import { ProvidedPortAttributes, RemoteTunnel, TunnelCreationOptions, TunnelOptions, TunnelPrivacyId } from 'vs/platform/tunnel/common/tunnel'; @@ -48,7 +48,7 @@ export interface IExtHostTunnelService extends ExtHostTunnelServiceShape { getTunnels(): Promise; onDidChangeTunnels: vscode.Event; setTunnelFactory(provider: vscode.RemoteAuthorityResolver | undefined): Promise; - registerPortsAttributesProvider(portSelector: { pid?: number; portRange?: [number, number]; commandPattern?: RegExp }, provider: vscode.PortAttributesProvider): IDisposable; + registerPortsAttributesProvider(portSelector: PortAttributesSelector, provider: vscode.PortAttributesProvider): IDisposable; } export const IExtHostTunnelService = createDecorator('IExtHostTunnelService'); @@ -74,7 +74,7 @@ export class ExtHostTunnelService implements IExtHostTunnelService { async setTunnelFactory(provider: vscode.RemoteAuthorityResolver | undefined): Promise { return { dispose: () => { } }; } - registerPortsAttributesProvider(portSelector: { pid?: number; portRange?: [number, number] }, provider: vscode.PortAttributesProvider) { + registerPortsAttributesProvider(portSelector: PortAttributesSelector, provider: vscode.PortAttributesProvider) { return { dispose: () => { } }; } diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 209c4623e92..ae3cf3568f8 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MainThreadTunnelServiceShape, MainContext, PortAttributesProviderSelector, TunnelDto } from 'vs/workbench/api/common/extHost.protocol'; +import { MainThreadTunnelServiceShape, MainContext, PortAttributesSelector, TunnelDto } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import type * as vscode from 'vscode'; import * as nls from 'vs/nls'; @@ -186,7 +186,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe private _initialCandidates: CandidatePort[] | undefined = undefined; private _providerHandleCounter: number = 0; - private _portAttributesProviders: Map = new Map(); + private _portAttributesProviders: Map = new Map(); constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @@ -232,7 +232,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe return this._providerHandleCounter++; } - registerPortsAttributesProvider(portSelector: PortAttributesProviderSelector, provider: vscode.PortAttributesProvider): vscode.Disposable { + registerPortsAttributesProvider(portSelector: PortAttributesSelector, provider: vscode.PortAttributesProvider): vscode.Disposable { const providerHandle = this.nextPortAttributesProviderHandle(); this._portAttributesProviders.set(providerHandle, { selector: portSelector, provider }); diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index 34f82543106..4b320bba521 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -70,7 +70,7 @@ export class BrowserDialogHandler extends AbstractDialogHandler { const buttons = this.getInputButtons(input); - const { button, checkboxChecked, values } = await this.doShow(input.type ?? 'question', input.message, buttons, input.detail, buttons.length - 1, input?.checkbox, input.inputs); + const { button, checkboxChecked, values } = await this.doShow(input.type ?? 'question', input.message, buttons, input.detail, buttons.length - 1, input?.checkbox, input.inputs, typeof input.custom === 'object' ? input.custom : undefined); return { confirmed: button === 0, checkboxChecked, values }; } diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index 0a7d082cea1..fe3397e05ec 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -18,6 +18,8 @@ import type { IProgress, IProgressCompositeOptions, IProgressDialogOptions, IPro import type { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import type { EditorGroupLayout } from 'vs/workbench/services/editor/common/editorGroupsService'; import type { IEmbedderTerminalOptions } from 'vs/workbench/services/terminal/common/embedderTerminalService'; +import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { ITranslations } from 'vs/platform/extensionManagement/common/extensionNls'; /** * The `IWorkbench` interface is the API facade for web embedders @@ -220,7 +222,7 @@ export interface IWorkbenchConstructionOptions { * - an extension in the Marketplace * - location of the extension where it is hosted. */ - readonly additionalBuiltinExtensions?: readonly (MarketplaceExtension | UriComponents)[]; + readonly additionalBuiltinExtensions?: readonly (MarketplaceExtension | UriComponents | HostedExtension)[]; /** * List of extensions to be enabled if they are installed. @@ -373,6 +375,15 @@ export interface IResourceUriProvider { export type ExtensionId = string; export type MarketplaceExtension = ExtensionId | { readonly id: ExtensionId; preRelease?: boolean; migrateStorageFrom?: ExtensionId }; +export interface HostedExtension { + readonly location: UriComponents; + readonly preRelease?: boolean; + readonly packageJSON?: IExtensionManifest; + readonly defaultPackageTranslations?: ITranslations | null; + readonly packageNLSUris?: Map; + readonly readmeUri?: UriComponents; + readonly changelogUri?: UriComponents; +} export interface ICommonTelemetryPropertiesResolver { (): { [key: string]: any }; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index d65c1ec6980..05cb749ccad 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -14,9 +14,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; -import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; import { runAccessibilityHelpAction } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; -import { clearChatEditor, clearChatSession } from 'vs/workbench/contrib/chat/browser/actions/chatClear'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; @@ -53,36 +51,6 @@ export function registerChatActions() { } }); - registerAction2(class ClearEditorAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chatEditor.clear', - title: { - value: localize('interactiveSession.clear.label', "Clear"), - original: 'Clear' - }, - icon: Codicon.clearAll, - f1: false, - menu: [{ - id: MenuId.EditorTitle, - group: 'navigation', - order: 0, - when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), - }] - }); - } - async run(accessor: ServicesAccessor, ...args: any[]) { - const widgetService = accessor.get(IChatWidgetService); - - const widget = widgetService.lastFocusedWidget; - if (!widget) { - return; - } - - await clearChatEditor(accessor, widget); - } - }); - registerAction2(class ClearChatHistoryAction extends Action2 { constructor() { super({ @@ -165,38 +133,6 @@ export function registerChatActions() { widgetService.lastFocusedWidget?.focusInput(); } }); - - registerAction2(class GlobalClearChatAction extends Action2 { - constructor() { - super({ - id: `workbench.action.chat.clear`, - title: { - value: localize('interactiveSession.clear.label', "Clear"), - original: 'Clear' - }, - category: CHAT_CATEGORY, - icon: Codicon.clearAll, - precondition: CONTEXT_PROVIDER_EXISTS, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.WinCtrl | KeyCode.KeyL, - when: CONTEXT_IN_CHAT_SESSION - } - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const widgetService = accessor.get(IChatWidgetService); - - const widget = widgetService.lastFocusedWidget; - if (!widget) { - return; - } - - await clearChatSession(accessor, widget); - } - }); } export function getOpenChatEditorAction(id: string, label: string, when?: string) { @@ -218,36 +154,6 @@ export function getOpenChatEditorAction(id: string, label: string, when?: string }; } -const getClearChatActionDescriptorForViewTitle = (viewId: string, providerId: string): Readonly & { viewId: string } => ({ - viewId, - id: `workbench.action.chat.${providerId}.clear`, - title: { - value: localize('interactiveSession.clear.label', "Clear"), - original: 'Clear' - }, - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', viewId), - group: 'navigation', - order: 0 - }, - category: CHAT_CATEGORY, - icon: Codicon.clearAll, - f1: false -}); - -export function getClearAction(viewId: string, providerId: string) { - return class ClearAction extends ViewAction { - constructor() { - super(getClearChatActionDescriptorForViewTitle(viewId, providerId)); - } - - async runInView(accessor: ServicesAccessor, view: ChatViewPane) { - await view.clear(); - } - }; -} - const getHistoryChatActionDescriptorForViewTitle = (viewId: string, providerId: string): Readonly & { viewId: string } => ({ viewId, id: `workbench.action.chat.${providerId}.history`, diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClear.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClear.ts index cb6795fbab4..f87796a56ef 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClear.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClear.ts @@ -5,16 +5,15 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IViewsService } from 'vs/workbench/common/views'; -import { IChatWidgetService, type IChatViewPane, type IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; +import { type IChatViewPane, type IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export async function clearChatSession(accessor: ServicesAccessor, widget: IChatWidget): Promise { - const viewsService = accessor.get(IViewsService); - if ('viewId' in widget.viewContext) { + const viewsService = accessor.get(IViewsService); // This cast is to break a circular dependency- ideally this would not be called directly for `/clear` // from the widget class, but from some contribution. const view = viewsService.getViewWithId(widget.viewContext.viewId); @@ -24,17 +23,19 @@ export async function clearChatSession(accessor: ServicesAccessor, widget: IChat (view as any as IChatViewPane).clear(); } else { - await clearChatEditor(accessor, widget); + await clearChatEditor(accessor); } } -export async function clearChatEditor(accessor: ServicesAccessor, widget: IChatWidget): Promise { +export async function clearChatEditor(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); const editorGroupsService = accessor.get(IEditorGroupsService); - const widgetService = accessor.get(IChatWidgetService); - await editorService.replaceEditors([{ - editor: editorService.activeEditor!, - replacement: { resource: ChatEditorInput.getNewEditorUri(), options: { target: { providerId: widgetService.lastFocusedWidget!.providerId, pinned: true } } } - }], editorGroupsService.activeGroup); + const chatEditorInput = editorService.activeEditor; + if (chatEditorInput instanceof ChatEditorInput && chatEditorInput.providerId) { + await editorService.replaceEditors([{ + editor: chatEditorInput, + replacement: { resource: ChatEditorInput.getNewEditorUri(), options: { target: { providerId: chatEditorInput.providerId, pinned: true } } } + }], editorGroupsService.activeGroup); + } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts new file mode 100644 index 00000000000..93ef5556d75 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { localize } from 'vs/nls'; +import { Action2, IAction2Options, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; +import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; +import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { clearChatEditor, clearChatSession } from 'vs/workbench/contrib/chat/browser/actions/chatClear'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; +import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; +import { CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; + +export function registerClearActions() { + + registerAction2(class ClearEditorAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chatEditor.clear', + title: { + value: localize('interactiveSession.clear.label', "Clear"), + original: 'Clear' + }, + icon: Codicon.clearAll, + f1: false, + menu: [{ + id: MenuId.EditorTitle, + group: 'navigation', + order: 0, + when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), + }] + }); + } + async run(accessor: ServicesAccessor, ...args: any[]) { + await clearChatEditor(accessor); + } + }); + + + registerAction2(class GlobalClearChatAction extends Action2 { + constructor() { + super({ + id: `workbench.action.chat.clear`, + title: { + value: localize('interactiveSession.clear.label', "Clear"), + original: 'Clear' + }, + category: CHAT_CATEGORY, + icon: Codicon.clearAll, + precondition: CONTEXT_PROVIDER_EXISTS, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.WinCtrl | KeyCode.KeyL, + when: CONTEXT_IN_CHAT_SESSION + } + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + const widgetService = accessor.get(IChatWidgetService); + + const widget = widgetService.lastFocusedWidget; + if (!widget) { + return; + } + + await clearChatSession(accessor, widget); + } + }); +} + +const getClearChatActionDescriptorForViewTitle = (viewId: string, providerId: string): Readonly & { viewId: string } => ({ + viewId, + id: `workbench.action.chat.${providerId}.clear`, + title: { + value: localize('interactiveSession.clear.label', "Clear"), + original: 'Clear' + }, + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', viewId), + group: 'navigation', + order: 0 + }, + category: CHAT_CATEGORY, + icon: Codicon.clearAll, + f1: false +}); + +export function getClearAction(viewId: string, providerId: string) { + return class ClearAction extends ViewAction { + constructor() { + super(getClearChatActionDescriptorForViewTitle(viewId, providerId)); + } + + async runInView(accessor: ServicesAccessor, view: ChatViewPane) { + await view.clear(); + } + }; +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts new file mode 100644 index 00000000000..9ea1e2fd1cc --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { localize } from 'vs/nls'; +import { Action2, IAction2Options, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; +import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; +import { IViewsService } from 'vs/workbench/common/views'; +import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; +import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; +import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; +import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +const getMoveToEditorChatActionDescriptorForViewTitle = (viewId: string, providerId: string): Readonly & { viewId: string } => ({ + id: `workbench.action.chat.${providerId}.openInEditor`, + title: { + value: localize('chat.openInEditor.label', "Open In Editor"), + original: 'Open In Editor' + }, + category: CHAT_CATEGORY, + icon: Codicon.arrowLeft, + precondition: CONTEXT_PROVIDER_EXISTS, + f1: false, + viewId, + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.deserialize('config.chat.experimiental.moveIcons')), + group: 'navigation', + order: 0 + }, +}); + +export function getMoveToEditorAction(viewId: string, providerId: string) { + return class MoveToEditorAction extends ViewAction { + constructor() { + super(getMoveToEditorChatActionDescriptorForViewTitle(viewId, providerId)); + } + + async runInView(accessor: ServicesAccessor, view: ChatViewPane) { + const viewModel = view.widget.viewModel; + if (!viewModel) { + return; + } + + const editorService = accessor.get(IEditorService); + view.clear(); + await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { target: { sessionId: viewModel.sessionId }, pinned: true } }); + } + }; +} + +const getMoveToSidebarChatActionDescriptorForViewTitle = (viewId: string, providerId: string): Readonly & { viewId: string } => ({ + id: `workbench.action.chat.${providerId}.openInSidebar`, + title: { + value: localize('chat.openInSidebar.label', "Open In Sidebar"), + original: 'Open In Sidebar' + }, + category: CHAT_CATEGORY, + icon: Codicon.arrowRight, + precondition: CONTEXT_PROVIDER_EXISTS, + f1: false, // TODO + viewId, + menu: [{ + id: MenuId.EditorTitle, + group: 'navigation', + order: 0, + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), ContextKeyExpr.deserialize('config.chat.experimental.moveIcons')), + }] +}); + +export function getMoveToSidebarAction(viewId: string, providerId: string) { + return class MoveToSidebarAction extends Action2 { + constructor() { + super(getMoveToSidebarChatActionDescriptorForViewTitle(viewId, providerId)); + } + + override async run(accessor: ServicesAccessor, ...args: any[]) { + return moveToSidebar(accessor); + } + }; +} + +async function moveToSidebar(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + const editorService = accessor.get(IEditorService); + const chatContribService = accessor.get(IChatContributionService); + const editorGroupService = accessor.get(IEditorGroupsService); + + const chatEditorInput = editorService.activeEditor; + if (chatEditorInput instanceof ChatEditorInput && chatEditorInput.sessionId && chatEditorInput.providerId) { + await editorService.closeEditor({ editor: chatEditorInput, groupId: editorGroupService.activeGroup.id }); + const viewId = chatContribService.getViewIdForProvider(chatEditorInput.providerId); + const view = await viewsService.openView(viewId) as ChatViewPane; + view.loadSession(chatEditorInput.sessionId); + } +} + +export function registerMoveActions() { + registerAction2(class GlobalMoveToEditorAction extends Action2 { + constructor() { + super({ + id: `workbench.action.chat.openInEditor`, + title: { + value: localize('interactiveSession.openInEditor.label', "Open Session In Editor"), + original: 'Open Session In Editor' + }, + category: CHAT_CATEGORY, + precondition: CONTEXT_PROVIDER_EXISTS, + f1: true + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + const widgetService = accessor.get(IChatWidgetService); + const viewService = accessor.get(IViewsService); + const editorService = accessor.get(IEditorService); + + const widget = widgetService.lastFocusedWidget; + if (!widget || !('viewId' in widget.viewContext)) { + return; + } + + const viewModel = widget.viewModel; + if (!viewModel) { + return; + } + + const view = await viewService.openView(widget.viewContext.viewId) as ChatViewPane; + await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { target: { sessionId: viewModel.sessionId }, pinned: true } }); + view.clear(); + } + }); + + registerAction2(class GlobalMoveToSidebarAction extends Action2 { + constructor() { + super({ + id: `workbench.action.chat.openInSidebar`, + title: { + value: localize('interactiveSession.openInSidebar.label', "Open Session In Sidebar"), + original: 'Open Session In Sidebar' + }, + category: CHAT_CATEGORY, + precondition: CONTEXT_PROVIDER_EXISTS, + f1: true + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + return moveToSidebar(accessor); + } + }); +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 40b6a5dc088..222d89dc682 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -9,11 +9,12 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatService, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; -import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellEditType, CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; @@ -33,9 +34,10 @@ export function registerChatTitleActions() { icon: Codicon.thumbsup, toggled: CONTEXT_RESPONSE_VOTE.isEqualTo('up'), menu: { - id: MenuId.ChatTitle, + id: MenuId.ChatMessageTitle, group: 'navigation', - order: 1 + order: 1, + when: CONTEXT_RESPONSE } }); } @@ -72,9 +74,10 @@ export function registerChatTitleActions() { icon: Codicon.thumbsdown, toggled: CONTEXT_RESPONSE_VOTE.isEqualTo('down'), menu: { - id: MenuId.ChatTitle, + id: MenuId.ChatMessageTitle, group: 'navigation', - order: 2 + order: 2, + when: CONTEXT_RESPONSE } }); } @@ -110,10 +113,10 @@ export function registerChatTitleActions() { category: CHAT_CATEGORY, icon: Codicon.insert, menu: { - id: MenuId.ChatTitle, + id: MenuId.ChatMessageTitle, group: 'navigation', isHiddenByDefault: true, - when: NOTEBOOK_IS_ACTIVE_EDITOR + when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, CONTEXT_RESPONSE) } }); } @@ -172,6 +175,40 @@ export function registerChatTitleActions() { } } }); + + + registerAction2(class RemoveAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.remove', + title: { + value: localize('chat.remove.label', "Remove Request and Response"), + original: 'Remove Request and Response' + }, + f1: false, + category: CHAT_CATEGORY, + icon: Codicon.x, + menu: { + id: MenuId.ChatMessageTitle, + group: 'navigation', + order: 2, + when: CONTEXT_REQUEST + } + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const item = args[0]; + if (!isRequestVM(item)) { + return; + } + + const chatService = accessor.get(IChatService); + if (item.providerRequestId) { + chatService.removeRequest(item.sessionId, item.providerRequestId); + } + } + }); } interface MarkdownContent { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index e798ba93bfd..5cc837e3bfa 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -35,6 +35,7 @@ import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbenc import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; +import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -128,6 +129,7 @@ registerChatTitleActions(); registerChatExecuteActions(); registerChatQuickQuestionActions(); registerChatExportActions(); +registerMoveActions(); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); registerSingleton(IChatContributionService, ChatContributionService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts index 5490cb2e1f7..94b1d453e1e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts @@ -16,7 +16,9 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from 'vs/workbench/common/views'; -import { getClearAction, getHistoryAction, getOpenChatEditorAction } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { getHistoryAction, getOpenChatEditorAction } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { getClearAction } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; +import { getMoveToEditorAction, getMoveToSidebarAction } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { IChatViewOptions, CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { IChatContributionService, IChatProviderContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -136,20 +138,23 @@ export class ChatContributionService implements IChatContributionService { }]; Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, viewContainer); + // Per-provider actions + // Actions in view title - const historyAction = registerAction2(getHistoryAction(viewId, providerDescriptor.id)); - const clearAction = registerAction2(getClearAction(viewId, providerDescriptor.id)); + const disposables = new DisposableStore(); + disposables.add(registerAction2(getHistoryAction(viewId, providerDescriptor.id))); + disposables.add(registerAction2(getClearAction(viewId, providerDescriptor.id))); + disposables.add(registerAction2(getMoveToEditorAction(viewId, providerDescriptor.id))); + disposables.add(registerAction2(getMoveToSidebarAction(viewId, providerDescriptor.id))); // "Open Chat Editor" Action - const openEditor = registerAction2(getOpenChatEditorAction(providerDescriptor.id, providerDescriptor.label, providerDescriptor.when)); + disposables.add(registerAction2(getOpenChatEditorAction(providerDescriptor.id, providerDescriptor.label, providerDescriptor.when))); return { dispose: () => { Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, viewContainer); Registry.as(ViewExtensions.ViewContainersRegistry).deregisterViewContainer(viewContainer); - clearAction.dispose(); - historyAction.dispose(); - openEditor.dispose(); + disposables.dispose(); } }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts b/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts index 42ed0215e03..8ec44820262 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts @@ -88,6 +88,7 @@ export class ChatEditorInput extends EditorInput { } this.sessionId = this.model.sessionId; + this.providerId = this.model.providerId; await this.model.waitForInitialization(); this._register(this.model.onDidChange(() => this._onDidChangeLabel.fire())); diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index f8decdfdd46..8ae6050e0c0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -54,12 +54,13 @@ import { IChatCodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/a import { IChatCodeBlockInfo } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatReplyFollowup, IChatService, ISlashCommand, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestViewModel, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; +import { marked } from 'vs/base/common/marked/marked'; const $ = dom.$; @@ -116,7 +117,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - if (action instanceof MenuItemAction) { + if (action instanceof MenuItemAction && (action.item.id === 'workbench.action.chat.voteDown' || action.item.id === 'workbench.action.chat.voteUp')) { return scopedInstantiationService.createInstance(ChatVoteButton, action, options as IMenuEntryActionViewItemOptions); } @@ -217,6 +218,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { } export class ChatAccessibilityProvider implements IListAccessibilityProvider { - getWidgetRole(): AriaRole { return 'list'; } @@ -534,7 +536,7 @@ export class ChatAccessibilityProvider implements IListAccessibilityProvider token.type === 'code')?.length ?? 0; + switch (codeBlockCount) { + case 0: + return element.response.value; + case 1: + return localize('singleCodeBlock', "1 code block, {0}", element.response.value); + default: + return localize('multiCodeBlock', "{0} code blocks, {1}", codeBlockCount, element.response.value); + } + } } interface IChatResultCodeBlockData { @@ -634,7 +648,6 @@ class CodeBlockPart extends Disposable implements IChatResultCodeBlockPart { ]) })); - this._register(this.options.onDidChange(() => { this.editor.updateOptions(this.getEditorOptionsFromConfig()); })); @@ -723,7 +736,7 @@ class CodeBlockPart extends Disposable implements IChatResultCodeBlockPart { this.setLanguage(vscodeLanguageId); this.layout(width); - + this.editor.updateOptions({ ariaLabel: localize('chat.codeBlockLabel', "Code block {0}", data.codeBlockIndex + 1) }); this.toolbar.context = { code: data.text, codeBlockIndex: data.codeBlockIndex, diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 4bfa2aae538..5f319d4eb0e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -115,8 +115,17 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { async clear(): Promise { if (this.widget.viewModel) { this.chatService.clearSession(this.widget.viewModel.sessionId); - this.updateModel(); } + this.updateModel(); + } + + loadSession(sessionId: string): void { + if (this.widget.viewModel) { + this.chatService.clearSession(this.widget.viewModel.sessionId); + } + + const newModel = this.chatService.getOrRestoreSession(sessionId); + this.updateModel(newModel); } focusInput(): void { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 37fc4519cbc..826123e4133 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -65,8 +65,8 @@ color: var(--vscode-badge-foreground) !important; } -.interactive-item-container:not(.interactive-response:hover) .header .monaco-toolbar, -.interactive-item-container:not(.interactive-response:hover) .header .monaco-toolbar .action-label { +.monaco-list-row:not(.focused) .interactive-item-container:not(:hover) .header .monaco-toolbar, +.monaco-list-row:not(.focused) .interactive-item-container:not(:hover) .header .monaco-toolbar .action-label { /* Also apply this rule to the .action-label directly to work around a strange issue- when the toolbar is hidden without that second rule, tabbing from the list container into a list item doesn't work and the tab key doesn't do anything. */ diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 8921237759a..7509c369304 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -10,6 +10,9 @@ export const CONTEXT_RESPONSE_HAS_PROVIDER_ID = new RawContextKey('chat export const CONTEXT_RESPONSE_VOTE = new RawContextKey('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); export const CONTEXT_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('chatSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") }); +export const CONTEXT_RESPONSE = new RawContextKey('chatResponse', false, { type: 'boolean', description: localize('chatResponse', "The chat item is a response.") }); +export const CONTEXT_REQUEST = new RawContextKey('chatRequest', false, { type: 'boolean', description: localize('chatRequest', "The chat item is a request") }); + export const CONTEXT_CHAT_INPUT_HAS_TEXT = new RawContextKey('chatInputHasText', false, { type: 'boolean', description: localize('interactiveInputHasText', "True when the chat input has text.") }); export const CONTEXT_IN_CHAT_INPUT = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const CONTEXT_IN_CHAT_SESSION = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index f614c7d38fb..21624ebe168 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -14,6 +14,7 @@ import { IChat, IChatFollowup, IChatProgress, IChatReplyFollowup, IChatResponse, export interface IChatRequestModel { readonly id: string; + readonly providerRequestId: string | undefined; readonly username: string; readonly avatarIconUri?: URI; readonly session: IChatModel; @@ -56,6 +57,10 @@ export class ChatRequestModel implements IChatRequestModel { return this._id; } + public get providerRequestId(): string | undefined { + return this._providerRequestId; + } + public get username(): string { return this.session.requesterUsername; } @@ -66,9 +71,14 @@ export class ChatRequestModel implements IChatRequestModel { constructor( public readonly session: ChatModel, - public readonly message: string | IChatReplyFollowup) { + public readonly message: string | IChatReplyFollowup, + private _providerRequestId?: string) { this._id = 'request_' + ChatRequestModel.nextId++; } + + setProviderRequestId(providerRequestId: string) { + this._providerRequestId = providerRequestId; + } } export class ChatResponseModel extends Disposable implements IChatResponseModel { @@ -189,7 +199,7 @@ export interface ISerializableChatsData { } export interface ISerializableChatRequestData { - providerResponseId: string | undefined; + providerRequestId: string | undefined; message: string; response: string | undefined; responseErrorDetails: IChatResponseErrorDetails | undefined; @@ -230,7 +240,7 @@ export function isSerializableSessionData(obj: unknown): obj is ISerializableCha typeof data.sessionId === 'string'; } -export type IChatChangeEvent = IChatAddRequestEvent | IChatAddResponseEvent | IChatInitEvent; +export type IChatChangeEvent = IChatAddRequestEvent | IChatAddResponseEvent | IChatInitEvent | IChatRemoveRequestEvent; export interface IChatAddRequestEvent { kind: 'addRequest'; @@ -242,6 +252,12 @@ export interface IChatAddResponseEvent { response: IChatResponseModel; } +export interface IChatRemoveRequestEvent { + kind: 'removeRequest'; + requestId: string; + responseId?: string; +} + export interface IChatInitEvent { kind: 'initialize'; } @@ -360,9 +376,9 @@ export class ChatModel extends Disposable implements IChatModel { } return requests.map((raw: ISerializableChatRequestData) => { - const request = new ChatRequestModel(this, raw.message); + const request = new ChatRequestModel(this, raw.message, raw.providerRequestId); if (raw.response || raw.responseErrorDetails) { - request.response = new ChatResponseModel(new MarkdownString(raw.response), this, true, raw.isCanceled, raw.vote, raw.providerResponseId, raw.responseErrorDetails, raw.followups); + request.response = new ChatResponseModel(new MarkdownString(raw.response), this, true, raw.isCanceled, raw.vote, raw.providerRequestId, raw.responseErrorDetails, raw.followups); } return request; }); @@ -433,7 +449,22 @@ export class ChatModel extends Disposable implements IChatModel { if ('content' in progress) { request.response.updateContent(progress.content); } else { - request.response.setProviderResponseId(progress.responseId); + request.setProviderRequestId(progress.requestId); + request.response.setProviderResponseId(progress.requestId); + } + } + + removeRequest(requestId: string): void { + const index = this._requests.findIndex(request => request.providerRequestId === requestId); + const request = this._requests[index]; + if (!request.providerRequestId) { + return; + } + + if (index !== -1) { + this._onDidChange.fire({ kind: 'removeRequest', requestId: request.providerRequestId, responseId: request.response?.providerResponseId }); + this._requests.splice(index, 1); + request.response?.dispose(); } } @@ -484,7 +515,7 @@ export class ChatModel extends Disposable implements IChatModel { }), requests: this._requests.map((r): ISerializableChatRequestData => { return { - providerResponseId: r.response?.providerResponseId, + providerRequestId: r.providerRequestId, message: typeof r.message === 'string' ? r.message : r.message.message, response: r.response ? r.response.response.value : undefined, responseErrorDetails: r.response?.errorDetails, diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 82d07f0c8d0..ea5a903a5e9 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -43,7 +43,7 @@ export interface IChatResponse { } export type IChatProgress = - { content: string } | { responseId: string }; + { content: string } | { requestId: string }; export interface IPersistedChatState { } export interface IChatProvider { @@ -56,6 +56,7 @@ export interface IChatProvider { provideFollowups?(session: IChat, token: CancellationToken): ProviderResult; provideReply(request: IChatRequest, progress: (progress: IChatProgress) => void, token: CancellationToken): ProviderResult; provideSlashCommands?(session: IChat, token: CancellationToken): ProviderResult; + removeRequest?(session: IChat, requestId: string): void; } export interface ISlashCommandProvider { @@ -186,6 +187,7 @@ export interface IChatService { * Returns whether the request was accepted. */ sendRequest(sessionId: string, message: string | IChatReplyFollowup): Promise<{ responseCompletePromise: Promise } | undefined>; + removeRequest(sessionid: string, requestId: string): Promise; cancelCurrentRequestForSession(sessionId: string): void; getSlashCommands(sessionId: string, token: CancellationToken): Promise; clearSession(sessionId: string): void; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 45ec1a33217..6126c29b26d 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -357,7 +357,7 @@ export class ChatService extends Disposable implements IChatService { if ('content' in progress) { this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`); } else { - this.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.responseId}`); + this.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`); } model.acceptResponseProgress(request, progress); @@ -414,6 +414,22 @@ export class ChatService extends Disposable implements IChatService { return rawResponsePromise; } + async removeRequest(sessionId: string, requestId: string): Promise { + const model = this._sessionModels.get(sessionId); + if (!model) { + throw new Error(`Unknown session: ${sessionId}`); + } + + await model.waitForInitialization(); + const provider = this._providers.get(model.providerId); + if (!provider) { + throw new Error(`Unknown provider: ${model.providerId}`); + } + + model.removeRequest(requestId); + 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 ?? []) { diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index ba66ad2c6cd..fb1cc56bb45 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -39,6 +39,8 @@ export interface IChatViewModel { export interface IChatRequestViewModel { readonly id: string; + readonly providerRequestId: string | undefined; + readonly sessionId: string; /** This ID updates every time the underlying data changes */ readonly dataId: string; readonly username: string; @@ -64,6 +66,7 @@ export interface IChatLiveUpdateData { export interface IChatResponseViewModel { readonly onDidChange: Event; readonly id: string; + readonly sessionId: string; /** This ID updates every time the underlying data changes */ readonly dataId: string; readonly providerId: string; @@ -91,7 +94,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; - private readonly _items: (IChatRequestViewModel | IChatResponseViewModel)[] = []; + private readonly _items: (ChatRequestViewModel | ChatResponseViewModel)[] = []; get inputPlaceholder(): string | undefined { return this._model.inputPlaceholder; @@ -135,6 +138,20 @@ export class ChatViewModel extends Disposable implements IChatViewModel { } } else if (e.kind === 'addResponse') { this.onAddResponse(e.response); + } else if (e.kind === 'removeRequest') { + const requestIdx = this._items.findIndex(item => isRequestVM(item) && item.providerRequestId === e.requestId); + if (requestIdx >= 0) { + this._items.splice(requestIdx, 1); + } + + const responseIdx = e.responseId && this._items.findIndex(item => isResponseVM(item) && item.providerResponseId === e.responseId); + if (typeof responseIdx === 'number' && responseIdx >= 0) { + const items = this._items.splice(responseIdx, 1); + const item = items[0]; + if (isResponseVM(item)) { + item.dispose(); + } + } } this._onDidChange.fire(); @@ -147,7 +164,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { this._items.push(response); } - getItems() { + getItems(): (IChatRequestViewModel | IChatResponseViewModel | IChatWelcomeMessageViewModel)[] { return [...(this._model.welcomeMessage ? [this._model.welcomeMessage] : []), ...this._items]; } @@ -164,10 +181,18 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.id; } + get providerRequestId() { + return this._model.providerRequestId; + } + get dataId() { return this.id + (this._model.session.isInitialized ? '' : '_initializing'); } + get sessionId() { + return this._model.session.sessionId; + } + get username() { return this._model.username; } @@ -211,6 +236,10 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.providerResponseId; } + get sessionId() { + return this._model.session.sessionId; + } + get username() { return this._model.username; } diff --git a/src/vs/workbench/contrib/experiments/common/experimentService.ts b/src/vs/workbench/contrib/experiments/common/experimentService.ts index 297b08a29af..24a46094bf9 100644 --- a/src/vs/workbench/contrib/experiments/common/experimentService.ts +++ b/src/vs/workbench/contrib/experiments/common/experimentService.ts @@ -233,7 +233,7 @@ export class ExperimentService extends Disposable implements IExperimentService return []; // TODO@sbatten add CLI argument (https://github.com/microsoft/vscode-internalbacklog/issues/2855) } - const experimentsUrl = this.configurationService.getValue('_workbench.experimentsUrl') || this.productService.experimentsUrl; + const experimentsUrl = this.productService.experimentsUrl; if (!experimentsUrl || this.configurationService.getValue('workbench.enableExperiments') === false) { return []; } diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts index 4946a918df2..d3fcd21a689 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts @@ -12,7 +12,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InteractiveEditorController, InteractiveEditorRunOptions } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController'; import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_HAS_PROVIDER, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK, CTX_INTERACTIVE_EDITOR_SHOWING_DIFF, CTX_INTERACTIVE_EDITOR_EDIT_MODE, EditMode, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE, CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE, CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED, CTX_INTERACTIVE_EDITOR_DID_EDIT } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; import { localize } from 'vs/nls'; -import { IAction2Options } from 'vs/platform/actions/common/actions'; +import { IAction2Options, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -25,6 +25,7 @@ import { Range } from 'vs/editor/common/core/range'; import { fromNow } from 'vs/base/common/date'; import { IInteractiveEditorSessionService, Recording } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorSession'; import { runAccessibilityHelpAction } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; +import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; export class StartSessionAction extends EditorAction2 { @@ -157,7 +158,7 @@ export class ArrowOutUpAction extends AbstractInteractiveEditorAction { super({ id: 'interactiveEditor.arrowOutUp', title: localize('arrowUp', 'Cursor Up'), - precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, EditorContextKeys.isEmbeddedDiffEditor.negate()), + precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, EditorContextKeys.isEmbeddedDiffEditor.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), keybinding: { weight: KeybindingWeight.EditorCore, primary: KeyCode.UpArrow @@ -175,7 +176,7 @@ export class ArrowOutDownAction extends AbstractInteractiveEditorAction { super({ id: 'interactiveEditor.arrowOutDown', title: localize('arrowDown', 'Cursor Down'), - precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, EditorContextKeys.isEmbeddedDiffEditor.negate()), + precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, EditorContextKeys.isEmbeddedDiffEditor.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), keybinding: { weight: KeybindingWeight.EditorCore, primary: KeyCode.DownArrow @@ -195,7 +196,7 @@ export class FocusInteractiveEditor extends EditorAction2 { id: 'interactiveEditor.focus', title: localize('focus', 'Focus'), category: AbstractInteractiveEditorAction.category, - precondition: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_FOCUSED.negate()), + precondition: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_FOCUSED.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), keybinding: [{ weight: KeybindingWeight.EditorCore + 10, // win against core_command when: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION.isEqualTo('above'), EditorContextKeys.isEmbeddedDiffEditor.negate()), @@ -251,6 +252,17 @@ export class NextFromHistory extends AbstractInteractiveEditorAction { } } +MenuRegistry.appendMenuItem(MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, { + submenu: MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD, + title: localize('discardMenu', "Discard..."), + icon: Codicon.discard, + group: '0_main', + order: 2, + when: CTX_INTERACTIVE_EDITOR_EDIT_MODE.notEqualsTo(EditMode.Preview), + rememberDefaultAction: true +}); + + export class DicardAction extends AbstractInteractiveEditorAction { constructor() { @@ -261,7 +273,7 @@ export class DicardAction extends AbstractInteractiveEditorAction { precondition: CTX_INTERACTIVE_EDITOR_VISIBLE, keybinding: { weight: KeybindingWeight.EditorContrib, - primary: KeyMod.Shift + KeyCode.Escape + primary: KeyMod.CtrlCmd + KeyCode.Escape }, menu: { id: MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD, diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts index 03c5622c67c..3e92d40d233 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts @@ -276,7 +276,7 @@ export class InteractiveEditorController implements IEditorContribution { this._activeSession!.recordExternalEditOccurred(editIsOutsideOfWholeRange); if (editIsOutsideOfWholeRange) { - this._logService.trace('[IE] text changed outside of whole range, FINISH session'); + this._logService.info('[IE] text changed outside of whole range, FINISH session'); this._finishExistingSession(); } })); diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorLivePreviewWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorLivePreviewWidget.ts index fff40debc32..b3ad4943088 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorLivePreviewWidget.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorLivePreviewWidget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Dimension, h } from 'vs/base/browser/dom'; -import { MutableDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { assertType } from 'vs/base/common/types'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; @@ -38,6 +38,7 @@ export class InteractiveEditorLivePreviewWidget extends ZoneWidget { private readonly _elements = h('div.interactive-editor-diff-widget@domNode'); + private readonly _sessionStore = this._disposables.add(new DisposableStore()); private readonly _diffEditor: IDiffEditor; private readonly _inlineDiffDecorations: IEditorDecorationsCollection; private _dim: Dimension | undefined; @@ -123,12 +124,27 @@ export class InteractiveEditorLivePreviewWidget extends ZoneWidget { override hide(): void { this._cleanupFullDiff(); this._cleanupInlineDiff(); + this._sessionStore.clear(); super.hide(); this._isVisible = false; } override show(): void { assertType(this.editor.hasModel()); + this._sessionStore.clear(); + + this._sessionStore.add(this._diffEditor.onDidUpdateDiff(() => { + const result = this._diffEditor.getDiffComputationResult(); + const hasFocus = this._diffEditor.hasTextFocus(); + this._updateFromChanges(this._session.wholeRange, result?.changes2 ?? []); + // TODO@jrieken find a better fix for this. this is the challenge: + // the _doShowForChanges method invokes show of the zone widget which removes and adds the + // zone and overlay parts. this dettaches and reattaches the dom nodes which means they lose + // focus + if (hasFocus) { + this._diffEditor.focus(); + } + })); this._updateFromChanges(this._session.wholeRange, this._session.lastTextModelChanges); this._isVisible = true; } diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorStrategies.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorStrategies.ts index 827ab314adc..c0d454e73ac 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorStrategies.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorStrategies.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ModifierKeyEmitter } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./interactiveEditor'; @@ -29,7 +30,7 @@ import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/ export abstract class EditModeStrategy { - dispose(): void { } + abstract dispose(): void; abstract checkChanges(response: EditResponse): boolean; @@ -67,7 +68,6 @@ export class PreviewStrategy extends EditModeStrategy { override dispose(): void { this._listener.dispose(); this._ctxDocumentChanged.reset(); - super.dispose(); } checkChanges(response: EditResponse): boolean { @@ -201,10 +201,11 @@ class InlineDiffDecorations { export class LiveStrategy extends EditModeStrategy { private static _inlineDiffStorageKey: string = 'interactiveEditor.storage.inlineDiff'; - private _inlineDiffEnabled: boolean = false; + protected _diffEnabled: boolean = false; private readonly _inlineDiffDecorations: InlineDiffDecorations; - protected readonly _ctxShowingDiff: IContextKey; + private readonly _ctxShowingDiff: IContextKey; + private readonly _diffToggleListener: IDisposable; private _lastResponse?: EditResponse; private _editCount: number = 0; @@ -219,28 +220,36 @@ export class LiveStrategy extends EditModeStrategy { @IInstantiationService private readonly _instaService: IInstantiationService, ) { super(); - this._inlineDiffDecorations = new InlineDiffDecorations(this._editor, this._inlineDiffEnabled); + this._inlineDiffDecorations = new InlineDiffDecorations(this._editor, this._diffEnabled); this._ctxShowingDiff = CTX_INTERACTIVE_EDITOR_SHOWING_DIFF.bindTo(contextKeyService); - this._inlineDiffEnabled = _storageService.getBoolean(LiveStrategy._inlineDiffStorageKey, StorageScope.PROFILE, false); - this._ctxShowingDiff.set(this._inlineDiffEnabled); - this._inlineDiffDecorations.visible = this._inlineDiffEnabled; + this._diffEnabled = _storageService.getBoolean(LiveStrategy._inlineDiffStorageKey, StorageScope.PROFILE, false); + this._ctxShowingDiff.set(this._diffEnabled); + this._inlineDiffDecorations.visible = this._diffEnabled; + this._diffToggleListener = ModifierKeyEmitter.getInstance().event(e => { + if (e.altKey || e.lastKeyReleased === 'alt') { + this.toggleDiff(); + } + }); } override dispose(): void { - this._inlineDiffEnabled = this._inlineDiffDecorations.visible; - this._storageService.store(LiveStrategy._inlineDiffStorageKey, this._inlineDiffEnabled, StorageScope.PROFILE, StorageTarget.USER); + this._diffToggleListener.dispose(); + this._diffEnabled = this._inlineDiffDecorations.visible; + this._storageService.store(LiveStrategy._inlineDiffStorageKey, this._diffEnabled, StorageScope.PROFILE, StorageTarget.USER); this._inlineDiffDecorations.clear(); this._ctxShowingDiff.reset(); - - super.dispose(); } toggleDiff(): void { - this._inlineDiffEnabled = !this._inlineDiffEnabled; - this._ctxShowingDiff.set(this._inlineDiffEnabled); - this._inlineDiffDecorations.visible = this._inlineDiffEnabled; - this._storageService.store(LiveStrategy._inlineDiffStorageKey, this._inlineDiffEnabled, StorageScope.PROFILE, StorageTarget.USER); + this._diffEnabled = !this._diffEnabled; + this._ctxShowingDiff.set(this._diffEnabled); + this._storageService.store(LiveStrategy._inlineDiffStorageKey, this._diffEnabled, StorageScope.PROFILE, StorageTarget.USER); + this._doToggleDiff(); + } + + protected _doToggleDiff(): void { + this._inlineDiffDecorations.visible = this._diffEnabled; } checkChanges(response: EditResponse): boolean { @@ -364,9 +373,10 @@ export class LivePreviewStrategy extends LiveStrategy { override async renderChanges(response: EditResponse) { - this._diffZone.show(); this._updateSummaryMessage(); - this._ctxShowingDiff.set(true); + if (this._diffEnabled) { + this._diffZone.show(); + } if (response.singleCreateFileEdit) { this._previewZone.showCreation(this._session.wholeRange, response.singleCreateFileEdit.uri, await Promise.all(response.singleCreateFileEdit.edits)); @@ -375,17 +385,16 @@ export class LivePreviewStrategy extends LiveStrategy { } } - override toggleDiff(): void { - // TODO@jrieken should this be persisted like we do in live-mode? - const scrollState = StableEditorScrollState.capture(this._editor); - if (this._diffZone.isVisible) { - this._diffZone.hide(); - this._ctxShowingDiff.set(false); - } else { - this._diffZone.show(); - this._ctxShowingDiff.set(true); + protected override _doToggleDiff(): void { + if (this._diffEnabled !== this._diffZone.isVisible) { + const scrollState = StableEditorScrollState.capture(this._editor); + if (this._diffEnabled) { + this._diffZone.show(); + } else { + this._diffZone.hide(); + } + scrollState.restore(this._editor); } - scrollState.restore(this._editor); } } diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts index 0c008cb9de4..ff558bfda11 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts @@ -289,7 +289,7 @@ export class InteractiveEditorWidget { return createActionViewItem(this._instantiationService, action, options); } }; - const statusToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.statusToolbar, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, workbenchToolbarOptions); + const statusToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.statusToolbar, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); this._store.add(statusToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); this._store.add(statusToolbar); diff --git a/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts b/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts index 34324a746b1..a08b00b0e38 100644 --- a/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; -import { Codicon } from 'vs/base/common/codicons'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IRange } from 'vs/editor/common/core/range'; @@ -12,7 +11,7 @@ import { ISelection } from 'vs/editor/common/core/selection'; import { ProviderResult, TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { localize } from 'vs/nls'; -import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { MenuId } from 'vs/platform/actions/common/actions'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -123,15 +122,6 @@ export const MENU_INTERACTIVE_EDITOR_WIDGET = MenuId.for('interactiveEditorWidge export const MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE = MenuId.for('interactiveEditorWidget.markdownMessage'); export const MENU_INTERACTIVE_EDITOR_WIDGET_STATUS = MenuId.for('interactiveEditorWidget.status'); export const MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD = MenuId.for('interactiveEditorWidget.undo'); -MenuRegistry.appendMenuItem(MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, { - submenu: MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD, - title: localize('discard', "Discard..."), - icon: Codicon.discard, - group: '0_main', - order: 2, - when: CTX_INTERACTIVE_EDITOR_EDIT_MODE.notEqualsTo(EditMode.Preview), - rememberDefaultAction: true -}); // --- colors diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts index d14d2de406c..c8e4a1fb3c0 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts @@ -59,15 +59,15 @@ export class KeybindingsSearchWidget extends SearchWidget { @IKeybindingService keybindingService: IKeybindingService, ) { super(parent, options, contextViewService, instantiationService, contextKeyService, keybindingService); + this._register(toDisposable(() => this.stopRecordingKeys())); + this._chords = null; this._inputValue = ''; - - this._reset(); } override clear(): void { - this._reset(); + this._chords = null; super.clear(); } @@ -81,7 +81,7 @@ export class KeybindingsSearchWidget extends SearchWidget { } stopRecordingKeys(): void { - this._reset(); + this._chords = null; this.recordDisposables.clear(); } @@ -90,10 +90,6 @@ export class KeybindingsSearchWidget extends SearchWidget { this.inputBox.value = this._inputValue; } - private _reset() { - this._chords = null; - } - private _onKeyDown(keyboardEvent: IKeyboardEvent): void { keyboardEvent.preventDefault(); keyboardEvent.stopPropagation(); @@ -103,10 +99,6 @@ export class KeybindingsSearchWidget extends SearchWidget { return; } if (keyboardEvent.equals(KeyCode.Escape)) { - if (this._chords !== null) { - this.clear(); - } - this._onEscape.fire(); return; } @@ -185,8 +177,8 @@ export class DefineKeybindingWidget extends Widget { this._keybindingInputWidget.startRecordingKeys(); this._register(this._keybindingInputWidget.onKeybinding(keybinding => this.onKeybinding(keybinding))); this._register(this._keybindingInputWidget.onEnter(() => this.hide())); - this._register(this._keybindingInputWidget.onEscape(() => this.onEscape())); - this._register(this._keybindingInputWidget.onBlur(() => this.onBlur())); + this._register(this._keybindingInputWidget.onEscape(() => this.clearOrHide())); + this._register(this._keybindingInputWidget.onBlur(() => this.onCancel())); this._outputNode = dom.append(this._domNode.domNode, dom.$('.output')); this._showExistingKeybindingsNode = dom.append(this._domNode.domNode, dom.$('.existing')); @@ -251,19 +243,15 @@ export class DefineKeybindingWidget extends Widget { dom.clearNode(this._outputNode); dom.clearNode(this._showExistingKeybindingsNode); - - const firstLabel = new KeybindingLabel(this._outputNode, OS, defaultKeybindingLabelStyles); firstLabel.set(withNullAsUndefined(this._chords?.[0])); - if (this._chords) { for (let i = 1; i < this._chords.length; i++) { this._outputNode.appendChild(document.createTextNode(nls.localize('defineKeybinding.chordsTo', "chord to"))); const chordLabel = new KeybindingLabel(this._outputNode, OS, defaultKeybindingLabelStyles); chordLabel.set(this._chords[i]); } - } const label = this.getUserSettingsLabel(); @@ -280,19 +268,20 @@ export class DefineKeybindingWidget extends Widget { return label; } - private onBlur(): void { + private onCancel(): void { this._chords = null; this.hide(); } - private onEscape(): void { - if (this._chords !== null) { + private clearOrHide(): void { + if (this._chords === null) { + this.hide(); + } else { this._chords = null; + this._keybindingInputWidget.clear(); dom.clearNode(this._outputNode); dom.clearNode(this._showExistingKeybindingsNode); - return; } - this.hide(); } private hide(): void { diff --git a/src/vs/workbench/contrib/remote/browser/remoteStartEntry.ts b/src/vs/workbench/contrib/remote/browser/remoteStartEntry.ts index 94c88c45d7e..555203e5cd1 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteStartEntry.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteStartEntry.ts @@ -162,12 +162,12 @@ export class RemoteStartEntry extends Disposable implements IWorkbenchContributi } private registerListeners(): void { - this._register(this.extensionManagementService.onDidInstallExtensions(async (result) => { - for (const ext of result) { - const index = this.remoteExtensionMetadata.findIndex(value => ExtensionIdentifier.equals(value.id, ext.identifier.id)); + this._register(this.extensionService.onDidChangeExtensions(async (result) => { + for (const ext of result.added) { + const index = this.remoteExtensionMetadata.findIndex(value => ExtensionIdentifier.equals(value.id, ext.identifier)); if (index > -1) { this.remoteExtensionMetadata[index].installed = true; - this.remoteExtensionMetadata[index].remoteCommands = await this.getRemoteCommands(ext.identifier.id); + this.remoteExtensionMetadata[index].remoteCommands = await this.getRemoteCommands(ext.identifier.value); } } })); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 08bd68007b8..7b8f2ce8d41 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1148,7 +1148,7 @@ class ViewModel { this._onDidChangeMode.fire(mode); this.modeContextKey.set(mode); - this.storageService.store(`scm.viewMode`, mode, StorageScope.WORKSPACE, StorageTarget.MACHINE); + this.storageService.store(`scm.viewMode`, mode, StorageScope.WORKSPACE, StorageTarget.USER); } get sortKey(): ViewModelSortKey { return this._sortKey; } @@ -1164,7 +1164,7 @@ class ViewModel { this.sortKeyContextKey.set(sortKey); if (this._mode === ViewModelMode.List) { - this.storageService.store(`scm.viewSortKey`, sortKey, StorageScope.WORKSPACE, StorageTarget.MACHINE); + this.storageService.store(`scm.viewSortKey`, sortKey, StorageScope.WORKSPACE, StorageTarget.USER); } } @@ -1243,6 +1243,20 @@ class ViewModel { if (e.reason === WillSaveStateReason.SHUTDOWN) { this.storageService.store(`scm.viewState`, JSON.stringify(this.treeViewState), StorageScope.WORKSPACE, StorageTarget.MACHINE); } + + this.mode = this.getViewModelMode(); + this.sortKey = this.getViewModelSortKey(); + }); + + this.storageService.onDidChangeValue(e => { + switch (e.key) { + case 'scm.viewMode': + this.mode = this.getViewModelMode(); + break; + case 'scm.viewSortKey': + this.sortKey = this.getViewModelSortKey(); + break; + } }); } diff --git a/src/vs/workbench/contrib/share/browser/share.contribution.ts b/src/vs/workbench/contrib/share/browser/share.contribution.ts index 7b8386eb3ed..a1819051c93 100644 --- a/src/vs/workbench/contrib/share/browser/share.contribution.ts +++ b/src/vs/workbench/contrib/share/browser/share.contribution.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./share'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; +import { MarkdownString } from 'vs/base/common/htmlContent'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { localize } from 'vs/nls'; import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -78,19 +80,23 @@ class ShareWorkbenchContribution { const uri = await shareService.provideShare({ resourceUri }, new CancellationTokenSource().token); if (uri) { - await clipboardService.writeText(uri.toString()); - const result = await dialogService.input( + const uriText = uri.toString(); + await clipboardService.writeText(uriText); + dialogService.prompt( { type: Severity.Info, - inputs: [{ type: 'text', value: uri.toString() }], message: localize('shareSuccess', 'Copied link to clipboard!'), - custom: { icon: Codicon.check }, - primaryButton: localize('open link', 'Open Link') + custom: { + icon: Codicon.check, + markdownDetails: [{ + markdown: new MarkdownString(`
${uriText}
`, { supportHtml: true }), + classes: ['share-dialog-input'] + }] + }, + cancelButton: localize('close', 'Close'), + buttons: [{ label: localize('open link', 'Open Link'), run: () => { urlService.open(uri, { openExternal: true }); } }] } ); - if (result.confirmed) { - urlService.open(uri, { openExternal: true }); - } } } }); diff --git a/src/vs/workbench/contrib/share/browser/share.css b/src/vs/workbench/contrib/share/browser/share.css new file mode 100644 index 00000000000..5d9469d49c6 --- /dev/null +++ b/src/vs/workbench/contrib/share/browser/share.css @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +div.share-dialog-input { + border: 1px solid var(--vscode-input-border, transparent); + border-radius: 2px; + color: var(--vscode-input-foreground); + background-color: var(--vscode-input-background); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding: 2px; + user-select: all; + line-height: 24px; +} + diff --git a/src/vs/workbench/contrib/share/browser/shareService.ts b/src/vs/workbench/contrib/share/browser/shareService.ts index 8e471a1593f..6fe98f2a5b0 100644 --- a/src/vs/workbench/contrib/share/browser/shareService.ts +++ b/src/vs/workbench/contrib/share/browser/shareService.ts @@ -6,6 +6,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { score } from 'vs/editor/common/languageSelector'; import { localize } from 'vs/nls'; import { ISubmenuItem } from 'vs/platform/actions/common/actions'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -46,7 +47,9 @@ export class ShareService implements IShareService { } async provideShare(item: IShareableItem, token: CancellationToken): Promise { - const providers = [...this._providers.values()]; + const providers = [...this._providers.values()] + .filter((p) => score(p.selector, item.resourceUri, '', true, undefined, undefined) > 0) + .sort((a, b) => a.priority - b.priority); if (providers.length === 0) { return undefined; diff --git a/src/vs/workbench/contrib/share/common/share.ts b/src/vs/workbench/contrib/share/common/share.ts index dcde5d780ad..b389d71c38b 100644 --- a/src/vs/workbench/contrib/share/common/share.ts +++ b/src/vs/workbench/contrib/share/common/share.ts @@ -19,6 +19,7 @@ export interface IShareableItem { export interface IShareProvider { readonly id: string; readonly label: string; + readonly priority: number; readonly selector: LanguageSelector; prepareShare?(item: IShareableItem, token: CancellationToken): Thenable; provideShare(item: IShareableItem, token: CancellationToken): Thenable; diff --git a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts index 0b9638e8e6b..e355b287f63 100644 --- a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts +++ b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts @@ -105,7 +105,9 @@ function getMergedDescription(collection: IMergedEnvironmentVariableCollection, } const workspaceDescription = workspaceDescriptions.get(ext); if (workspaceDescription) { - message.push(`\n- \`${getExtensionName(ext, extensionService)} (${localize('ScopedEnvironmentContributionInfo', 'workspace')})\``); + // Only show '(workspace)' suffix if there is already a description for the extension. + const workspaceSuffix = globalDescription ? ` (${localize('ScopedEnvironmentContributionInfo', 'workspace')})` : ''; + message.push(`\n- \`${getExtensionName(ext, extensionService)}${workspaceSuffix}\``); message.push(`: ${workspaceDescription}`); } if (!globalDescription && !workspaceDescription) { diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index e910b297258..9b5149079b4 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -735,7 +735,8 @@ export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [ 'workbench.action.togglePanel', 'workbench.action.quickOpenView', 'workbench.action.toggleMaximizedPanel', - 'notification.acceptPrimaryAction' + 'notification.acceptPrimaryAction', + 'runCommands' ]; export const terminalContributionsDescriptor: IExtensionPointDescriptor = { diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts index b5894759b3f..f1ae17cb789 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts @@ -87,7 +87,10 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes // when test states change, reflect in the tree this._register(results.onTestChanged(ev => { let result = ev.item; - if (result.ownComputedState === TestResultState.Unset) { + // if the state is unset, or the latest run is not making the change, + // double check that it's valid. Retire calls might cause previous + // emit a state change for a test run that's already long completed. + if (result.ownComputedState === TestResultState.Unset || ev.result !== results.results[0]) { const fallback = results.getStateById(result.item.extId); if (fallback) { result = fallback[1]; diff --git a/src/vs/workbench/contrib/testing/common/testResult.ts b/src/vs/workbench/contrib/testing/common/testResult.ts index 8627db9c2bc..c2130600cdb 100644 --- a/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/src/vs/workbench/contrib/testing/common/testResult.ts @@ -449,6 +449,18 @@ export class LiveTestResult implements ITestResult { this.completeEmitter.fire(); } + /** + * Marks the test and all of its children in the run as retired. + */ + public markRetired(testId: string) { + for (const [id, test] of this.testById) { + if (!test.retired && id === testId || TestId.isChild(testId, id)) { + test.retired = true; + this.changeEmitter.fire({ reason: TestResultItemChangeReason.ComputedStateChange, item: test, result: this }); + } + } + } + /** * @inheritdoc */ diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index a740cd40d8a..99590868fd0 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -469,12 +469,14 @@ export interface TestResultItem extends InternalTestItem { } export namespace TestResultItem { - /** Serialized version of the TestResultItem */ + /** + * Serialized version of the TestResultItem. Note that 'retired' is not + * included since all hydrated items are automatically retired. + */ export interface Serialized extends InternalTestItem.Serialized { tasks: ITestTaskState.Serialized[]; ownComputedState: TestResultState; computedState: TestResultState; - retired?: boolean; } export const serializeWithoutMessages = (original: TestResultItem): Serialized => ({ @@ -482,7 +484,6 @@ export namespace TestResultItem { ownComputedState: original.ownComputedState, computedState: original.computedState, tasks: original.tasks.map(ITestTaskState.serializeWithoutMessages), - retired: original.retired, }); export const serialize = (original: TestResultItem): Serialized => ({ @@ -490,7 +491,6 @@ export namespace TestResultItem { ownComputedState: original.ownComputedState, computedState: original.computedState, tasks: original.tasks.map(ITestTaskState.serialize), - retired: original.retired, }); export const deserialize = (serialized: Serialized): TestResultItem => ({ diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts index 44b67eaea34..502b66a9052 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts @@ -23,6 +23,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { setup(() => { onTestChanged = new Emitter(); resultsService = { + results: [], onResultsChanged: () => undefined, onTestChanged: onTestChanged.event, getStateById: () => ({ state: { state: 0 }, computedState: 0 }), @@ -102,7 +103,6 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { test('applies state changes', async () => { harness.flush(); - resultsService.getStateById = () => [undefined, resultInState(TestResultState.Failed)]; const resultInState = (state: TestResultState): TestResultItem => ({ item: { @@ -124,6 +124,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { }); // Applies the change: + resultsService.getStateById = () => [undefined, resultInState(TestResultState.Queued)]; onTestChanged.fire({ reason: TestResultItemChangeReason.OwnStateChange, result: null as any, @@ -139,6 +140,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { ]); // Falls back if moved into unset state: + resultsService.getStateById = () => [undefined, resultInState(TestResultState.Failed)]; onTestChanged.fire({ reason: TestResultItemChangeReason.OwnStateChange, result: null as any, diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index c7d1435d648..838ac8e0ed5 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -73,7 +73,6 @@ import { IFeaturedExtensionsService } from 'vs/workbench/contrib/welcomeGettingS import { IFeaturedExtension } from 'vs/base/common/product'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; const SLIDE_TRANSITION_TIME_MS = 250; const configurationKey = 'workbench.startupEditor'; @@ -190,9 +189,7 @@ export class GettingStartedPage extends EditorPane { @IWebviewService private readonly webviewService: IWebviewService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @ILifecycleService private readonly lifecycleService: ILifecycleService, - ) { + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService) { super(GettingStartedPage.ID, telemetryService, themeService, storageService); @@ -305,7 +302,6 @@ export class GettingStartedPage extends EditorPane { this.container.classList.remove('animatable'); this.editorInput = newInput; await super.setInput(newInput, options, context, token); - await this.lifecycleService.when(LifecyclePhase.Restored); await this.buildCategoriesSlide(); if (this.shouldAnimate()) { setTimeout(() => this.container.classList.add('animatable'), 0); @@ -729,6 +725,9 @@ export class GettingStartedPage extends EditorPane { private async buildCategoriesSlide() { + // Delay fetching welcome page content on startup until all extensions are ready. + await this.extensionService.whenInstalledExtensionsRegistered(); + this.recentlyOpened = await this.workspacesService.getRecentlyOpened(); this.gettingStartedCategories = await this.gettingStartedService.getWalkthroughs(); this.featuredExtensions = await this.featuredExtensionService.getExtensions(); diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index e83df848d93..cb24963a2e6 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -45,6 +45,15 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; type GalleryExtensionInfo = { readonly id: string; preRelease?: boolean; migrateStorageFrom?: string }; +interface HostedExtensionInfo { + readonly location: UriComponents; + readonly preRelease?: boolean; + readonly packageJSON?: IExtensionManifest; + readonly defaultPackageTranslations?: ITranslations | null; + readonly packageNLSUris?: Map; + readonly readmeUri?: UriComponents; + readonly changelogUri?: UriComponents; +} type ExtensionInfo = { readonly id: string; preRelease: boolean }; function isGalleryExtensionInfo(obj: unknown): obj is GalleryExtensionInfo { @@ -54,6 +63,24 @@ function isGalleryExtensionInfo(obj: unknown): obj is GalleryExtensionInfo { && (galleryExtensionInfo.migrateStorageFrom === undefined || typeof galleryExtensionInfo.migrateStorageFrom === 'string'); } +function isHostedExtensionInfo(obj: unknown): obj is HostedExtensionInfo { + const hostedExtensionInfo = obj as HostedExtensionInfo | undefined; + return isUriComponents(hostedExtensionInfo?.location) + && (hostedExtensionInfo?.preRelease === undefined || typeof hostedExtensionInfo.preRelease === 'boolean') + && (hostedExtensionInfo?.packageJSON === undefined || typeof hostedExtensionInfo.packageJSON === 'object') + && (hostedExtensionInfo?.defaultPackageTranslations === undefined || hostedExtensionInfo?.defaultPackageTranslations === null || typeof hostedExtensionInfo.defaultPackageTranslations === 'object') + && (hostedExtensionInfo?.changelogUri === undefined || isUriComponents(hostedExtensionInfo?.changelogUri)) + && (hostedExtensionInfo?.readmeUri === undefined || isUriComponents(hostedExtensionInfo?.readmeUri)); +} + +function isUriComponents(thing: unknown): thing is UriComponents { + if (!thing) { + return false; + } + return isString((thing).path) && + isString((thing).scheme); +} + interface IStoredWebExtension { readonly identifier: IExtensionIdentifier; readonly version: string; @@ -117,12 +144,12 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } } - private _customBuiltinExtensionsInfoPromise: Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[] }> | undefined; - private readCustomBuiltinExtensionsInfoFromEnv(): Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[] }> { + private _customBuiltinExtensionsInfoPromise: Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: HostedExtensionInfo[] }> | undefined; + private readCustomBuiltinExtensionsInfoFromEnv(): Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: HostedExtensionInfo[] }> { if (!this._customBuiltinExtensionsInfoPromise) { this._customBuiltinExtensionsInfoPromise = (async () => { let extensions: ExtensionInfo[] = []; - const extensionLocations: URI[] = []; + const extensionLocations: HostedExtensionInfo[] = []; const extensionsToMigrate: [string, string][] = []; const customBuiltinExtensionsInfo = this.environmentService.options && Array.isArray(this.environmentService.options.additionalBuiltinExtensions) ? this.environmentService.options.additionalBuiltinExtensions.map(additionalBuiltinExtension => isString(additionalBuiltinExtension) ? { id: additionalBuiltinExtension } : additionalBuiltinExtension) @@ -134,7 +161,11 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten extensionsToMigrate.push([e.migrateStorageFrom, e.id]); } } else { - extensionLocations.push(URI.revive(e)); + if (isHostedExtensionInfo(e)) { + extensionLocations.push(e); + } else { + extensionLocations.push({ location: e }); + } } } if (extensions.length) { @@ -212,9 +243,15 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return []; } const result: IScannedExtension[] = []; - await Promise.allSettled(extensionLocations.map(async location => { + await Promise.allSettled(extensionLocations.map(async ({ location, preRelease, packageNLSUris, packageJSON, defaultPackageTranslations, readmeUri, changelogUri }) => { try { - const webExtension = await this.toWebExtension(location); + const webExtension = await this.toWebExtension(URI.revive(location), undefined, + packageJSON, + packageNLSUris ? [...packageNLSUris.entries()].reduce((result, [key, value]) => { result.set(key, URI.revive(value)); return result; }, new Map()) : undefined, + defaultPackageTranslations, + URI.revive(readmeUri), + URI.revive(changelogUri), + { isPreReleaseVersion: preRelease }); const extension = await this.toScannedExtension(webExtension, true); if (extension.isValid || !scanOptions?.skipInvalidExtensions) { result.push(extension); @@ -438,7 +475,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } async addExtension(location: URI, metadata: Metadata, profileLocation: URI): Promise { - const webExtension = await this.toWebExtension(location, undefined, undefined, undefined, undefined, undefined, metadata); + const webExtension = await this.toWebExtension(location, undefined, undefined, undefined, undefined, undefined, undefined, metadata); const extension = await this.toScannedExtension(webExtension, false); await this.addToInstalledExtensions([webExtension], profileLocation); return extension; @@ -577,6 +614,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return this.toWebExtension( extensionLocation, galleryExtension.identifier, + undefined, packageNLSResources, fallbackPackageNLSResource ? URI.parse(fallbackPackageNLSResource) : null, galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined, @@ -596,12 +634,13 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return packageNLSResources; } - private async toWebExtension(extensionLocation: URI, identifier?: IExtensionIdentifier, packageNLSUris?: Map, fallbackPackageNLSUri?: URI | null, readmeUri?: URI, changelogUri?: URI, metadata?: Metadata): Promise { - let manifest: IExtensionManifest; - try { - manifest = await this.getExtensionManifest(extensionLocation); - } catch (error) { - throw new Error(`Error while fetching manifest from the location '${extensionLocation.toString()}'. ${getErrorMessage(error)}`); + private async toWebExtension(extensionLocation: URI, identifier?: IExtensionIdentifier, manifest?: IExtensionManifest, packageNLSUris?: Map, fallbackPackageNLSUri?: URI | ITranslations | null, readmeUri?: URI, changelogUri?: URI, metadata?: Metadata): Promise { + if (!manifest) { + try { + manifest = await this.getExtensionManifest(extensionLocation); + } catch (error) { + throw new Error(`Error while fetching manifest from the location '${extensionLocation.toString()}'. ${getErrorMessage(error)}`); + } } if (!this.extensionManifestPropertiesService.canExecuteOnWeb(manifest)) { @@ -616,7 +655,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten fallbackPackageNLSUri = undefined; } } - const defaultManifestTranslations: ITranslations | null | undefined = fallbackPackageNLSUri ? await this.getTranslations(fallbackPackageNLSUri) : null; + const defaultManifestTranslations: ITranslations | null | undefined = fallbackPackageNLSUri ? URI.isUri(fallbackPackageNLSUri) ? await this.getTranslations(fallbackPackageNLSUri) : fallbackPackageNLSUri : null; return { identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name), uuid: identifier?.uuid }, @@ -626,7 +665,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten readmeUri, changelogUri, packageNLSUris, - fallbackPackageNLSUri: fallbackPackageNLSUri ? fallbackPackageNLSUri : undefined, + fallbackPackageNLSUri: URI.isUri(fallbackPackageNLSUri) ? fallbackPackageNLSUri : undefined, defaultManifestTranslations, metadata, }; diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index f1c61165b4c..9da19f74bda 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -83,6 +83,7 @@ export const allApiProposals = Object.freeze({ terminalDimensions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts', terminalQuickFixProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts', testCoverage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testCoverage.d.ts', + testInvalidateResults: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testInvalidateResults.d.ts', testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', timeline: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts', diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 6920a263671..bebdacc22ba 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -507,7 +507,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex let sourceTextModel: ITextModel | undefined = undefined; if (sourceModel instanceof BaseTextEditorModel) { if (sourceModel.isResolved()) { - sourceTextModel = sourceModel.textEditorModel; + sourceTextModel = withNullAsUndefined(sourceModel.textEditorModel); } } else { sourceTextModel = sourceModel as ITextModel; diff --git a/src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts b/src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts index a2041267575..4c443c006a1 100644 --- a/src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts +++ b/src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts @@ -27,6 +27,7 @@ suite('DomActivityTracker', () => { clock.restore(); }); + test('marks inactive on no input', () => { assert.equal(uas.isActive, true); clock.tick(maxTimeToBecomeIdle); diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index c09be287b81..0ef30439611 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -151,6 +151,9 @@ declare module 'vscode' { prepareSession(initialState: InteractiveSessionState | undefined, token: CancellationToken): ProviderResult; resolveRequest(session: S, context: InteractiveSessionRequestArgs | string, token: CancellationToken): ProviderResult; provideResponseWithProgress(request: InteractiveRequest, progress: Progress, token: CancellationToken): ProviderResult; + + // eslint-disable-next-line local/vscode-dts-provider-naming + removeRequest(session: S, requestId: string): void; } export interface InteractiveSessionDynamicRequest { diff --git a/src/vscode-dts/vscode.proposed.portsAttributes.d.ts b/src/vscode-dts/vscode.proposed.portsAttributes.d.ts index 6f07fe7aadf..cf130357ead 100644 --- a/src/vscode-dts/vscode.proposed.portsAttributes.d.ts +++ b/src/vscode-dts/vscode.proposed.portsAttributes.d.ts @@ -7,6 +7,9 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/115616 @alexr00 + /** + * The action that should be taken when a port is discovered through automatic port forwarding discovery. + */ export enum PortAutoForwardAction { /** * Notify the user that the port is being forwarded. This is the default action. @@ -67,21 +70,16 @@ declare module 'vscode' { providePortAttributes(port: number, pid: number | undefined, commandLine: string | undefined, token: CancellationToken): ProviderResult; } - export interface PortAttributesProviderSelector { - /** - * TODO: @alexr00 no one is currently using this. Should we delete it? - * If your {@link PortAttributesProvider PortAttributesProvider} is registered after your process has started then already know the process id of port you are listening on. - * Specifying a pid will cause your provider to only be called for ports that match the pid. - */ - pid?: number; - + /** + * A selector that will be used to filter which {@link PortAttributesProvider} should be called for each port. + */ + export interface PortAttributesSelector { /** * Specifying a port range will cause your provider to only be called for ports within the range. */ portRange?: [number, number]; /** - * TODO: @alexr00 no one is currently using this. Should we delete it? * Specifying a command pattern will cause your provider to only be called for processes whose command line matches the pattern. */ commandPattern?: RegExp; @@ -100,6 +98,6 @@ declare module 'vscode' { * If you don't specify a port selector your provider will be called for every port, which will result in slower port forwarding for the user. * @param provider The {@link PortAttributesProvider PortAttributesProvider}. */ - export function registerPortAttributesProvider(portSelector: PortAttributesProviderSelector, provider: PortAttributesProvider): Disposable; + export function registerPortAttributesProvider(portSelector: PortAttributesSelector, provider: PortAttributesProvider): Disposable; } } diff --git a/src/vscode-dts/vscode.proposed.testInvalidateResults.d.ts b/src/vscode-dts/vscode.proposed.testInvalidateResults.d.ts new file mode 100644 index 00000000000..35f955c2f4d --- /dev/null +++ b/src/vscode-dts/vscode.proposed.testInvalidateResults.d.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// https://github.com/microsoft/vscode/issues/134970 + +declare module 'vscode' { + + export interface TestController { + + /** + * Marks an item's results as being outdated. This is commonly called when + * code or configuration changes and previous results should no longer + * be considered relevant. The same logic used to mark results as outdated + * may be used to drive {@link TestRunRequest.continuous continuous test runs}. + * + * If an item is passed to this method, test results for the item and all of + * its children will be marked as outdated. If no item is passed, then all + * test owned by the TestController will be marked as outdated. + * + * Any test runs started before the moment this method is called, including + * runs which may still be ongoing, will be marked as outdated and deprioritized + * in the editor's UI. + * + * @param item Item to mark as outdated. If undefined, all the controller's items are marked outdated. + */ + invalidateTestResults(item?: TestItem): void; + } +} diff --git a/yarn.lock b/yarn.lock index ed38da917db..b4adddd0a59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9782,10 +9782,10 @@ typescript@^4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== -typescript@^5.2.0-dev.20230516: - version "5.2.0-dev.20230516" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.0-dev.20230516.tgz#9dc4d02c031cdf311ddb6ee646be09d5f13de4c9" - integrity sha512-DGK8md4PQgA6QG9JnvC6LecNnBstc1h6zrg71isrlZTsRFVl3EsID6D2Exrh4ULbxynA61PE13M+uOmLFWOu4w== +typescript@^5.2.0-dev.20230524: + version "5.2.0-dev.20230524" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.0-dev.20230524.tgz#c2714b82f5ef8c63328a253692ae0b8c244c86d6" + integrity sha512-1XzSUJCt31jm7jIZ3vBKzK46ZxnmqX2VdVg/dur9AIaz9WmidrABs7F8H8d4onpIV8RYD/L6xW6MXR5EHjl+LA== typical@^4.0.0: version "4.0.0"