Merge branch 'main' into tyriar/183236

This commit is contained in:
Daniel Imms 2023-05-25 04:54:33 -07:00 committed by GitHub
commit baa3e220bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
101 changed files with 1559 additions and 388 deletions

View file

@ -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<RemoteSourceAction[]> {
return getRemoteSourceActions(this._model, url);
}
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable {
return this._model.registerRemoteSourceProvider(provider);
}

View file

@ -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<string[]>;
getRemoteSourceActions?(url: string): ProviderResult<RemoteSourceAction[]>;
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}

View file

@ -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<RemoteSourceAction[]> {
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<string | undefined>;
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch: true }): Promise<PickRemoteSourceResult | undefined>;
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {

View file

@ -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<Repository | null> {
async init(root: Uri, options?: InitOptions): Promise<Repository | null> {
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';

View file

@ -8,6 +8,7 @@ export { ProviderResult } from 'vscode';
export interface API {
pickRemoteSource(options: PickRemoteSourceOptions): Promise<string | PickRemoteSourceResult | undefined>;
getRemoteSourceActions(url: string): Promise<RemoteSourceAction[]>;
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<string[]>;
getRemoteSourceActions?(url: string): ProviderResult<RemoteSourceAction[]>;
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}

View file

@ -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<Repository | null>;
init(root: Uri, options?: InitOptions): Promise<Repository | null>;
openRepository(root: Uri): Promise<Repository | null>
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;

View file

@ -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<void> {
if (!this.ref.name) {
@ -278,7 +282,54 @@ async function createCheckoutItems(repository: Repository, detached = false): Pr
}
}
return processors.reduce<CheckoutItem[]>((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<CheckoutItem[]>((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<string, RemoteSourceActionButton[]>();
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<QuickPickItem | undefined>(c => quickpick.onDidAccept(() => c(quickpick.activeItems[0])));
const choice = await new Promise<QuickPickItem | undefined>(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) {

View file

@ -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);
}
}

View file

@ -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<void> {
async init(repository: string, options: InitOptions = {}): Promise<void> {
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) {

View file

@ -11,3 +11,7 @@ export async function pickRemoteSource(options: PickRemoteSourceOptions & { bran
export async function pickRemoteSource(options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {
return GitBaseApi.getAPI().pickRemoteSource(options);
}
export async function getRemoteSourceActions(url: string) {
return GitBaseApi.getAPI().getRemoteSourceActions(url);
}

View file

@ -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;

View file

@ -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 {

View file

@ -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`;
}

View file

@ -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;

View file

@ -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<RemoteSourceAction[]> {
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));
}
}];
}
}

View file

@ -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<string[]>;
getRemoteSourceActions?(url: string): ProviderResult<RemoteSourceAction[]>;
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}

View file

@ -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<Repository | null>;
init(root: Uri, options?: InitOptions): Promise<Repository | null>;
openRepository(root: Uri): Promise<Repository | null>
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;

View file

@ -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'));

View file

@ -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"
}
]
},

View file

@ -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": [
{

View file

@ -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"
}
]
}
}
}

View file

@ -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": {

View file

@ -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"

View file

@ -14,6 +14,7 @@ import { getDocumentDir } from '../../util/document';
enum MediaKind {
Image,
Video,
Audio,
}
export const mediaFileExtensions = new Map<string, MediaKind>([
@ -35,6 +36,11 @@ export const mediaFileExtensions = new Map<string, MediaKind>([
// 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(`<video src="${mdPath}" controls title="`);
snippet.appendPlaceholder('Title');
snippet.appendText('"></video>');
} else if (insertAsAudio) {
insertedAudioVideoCount++;
snippet.appendText(`<audio src="${mdPath}" controls title="`);
snippet.appendPlaceholder('Title');
snippet.appendText('"></audio>');
} 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

View file

@ -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);
}

View file

@ -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<T>(_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 content</') === -1, `Content was not cleared: ${outputElement.innerHTML}`);
assert.ok(inserted.innerHTML.indexOf('>second stream content</') === -1, `Content was not cleared: ${outputElement.innerHTML}`);
assert.ok(inserted.innerHTML.indexOf('>third stream content</') === -1, `Content was not cleared: ${outputElement.innerHTML}`);
});
test(`Rendered output will wrap on settings change event`, async () => {
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);
});
});

View file

@ -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'

View file

@ -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"

View file

@ -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%",

View file

@ -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.",

View file

@ -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)) {

View file

@ -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<boolean> {
public async execute({ document, action, diagnostic, followupAction }: ApplyCodeActionCommand_args): Promise<boolean> {
/* __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<VsCodeCode
diagnostic: vscode.Diagnostic,
tsAction: Proto.CodeFixAction
): CodeActionSet {
results.addAction(this.getSingleFixForTsCodeAction(document.uri, diagnostic, tsAction));
results.addAction(this.getSingleFixForTsCodeAction(document, diagnostic, tsAction));
this.addFixAllForTsCodeAction(results, document.uri, file, diagnostic, tsAction as Proto.CodeFixAction);
return results;
}
private getSingleFixForTsCodeAction(
resource: vscode.Uri,
document: vscode.TextDocument,
diagnostic: vscode.Diagnostic,
tsAction: Proto.CodeFixAction
): VsCodeCodeAction {
const aiQuickFixEnabled = vscode.workspace.getConfiguration('typescript').get('experimental.aiQuickFix');
let followupAction: Command | undefined;
if (aiQuickFixEnabled && tsAction.fixName === fixNames.classIncorrectlyImplementsInterface) {
followupAction = new EditorChatFollowUp('Implement the class using the interface', document, diagnostic.range, this.client);
}
const codeAction = new VsCodeCodeAction(tsAction, tsAction.description, vscode.CodeActionKind.QuickFix);
codeAction.edit = getEditForCodeAction(this.client, tsAction);
codeAction.diagnostics = [diagnostic];
codeAction.command = {
command: ApplyCodeActionCommand.ID,
arguments: [<ApplyCodeActionCommand_args>{ action: tsAction, diagnostic, resource }],
arguments: [<ApplyCodeActionCommand_args>{ action: tsAction, diagnostic, document, followupAction }],
title: ''
};
return codeAction;

View file

@ -43,10 +43,13 @@
"tokenInformation",
"treeItemCheckbox",
"treeViewReveal",
"testInvalidateResults",
"workspaceTrust",
"telemetry",
"windowActivity",
"interactiveUserActions"
"interactiveUserActions",
"envCollectionWorkspace",
"envCollectionOptions"
],
"private": true,
"activationEvents": [],

View file

@ -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",

View file

@ -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`,

View file

@ -18,6 +18,8 @@ suite('dom', () => {
assert(!element.classList.contains('bar'));
assert(!element.classList.contains('foo'));
assert(!element.classList.contains(''));
});
test('removeClass', () => {

View file

@ -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';

View file

@ -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');
/**

View file

@ -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);
}
});

View file

@ -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));
}

View file

@ -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 {

View file

@ -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
*/

View file

@ -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<number, PortAttributesProviderSelector> = new Map();
private portsAttributesProviders: Map<number, PortAttributesSelector> = 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<void> {
async $registerPortsAttributesProvider(selector: PortAttributesSelector, providerHandle: number): Promise<void> {
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) {

View file

@ -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>('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<IExtensionStatusBarItemChangeEvent>;
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<ExtensionStatusBarEntry>;
}
@ -46,8 +55,17 @@ class ExtensionStatusBarItemService implements IExtensionStatusBarItemService {
private readonly _entries: Map<string, { accessor: IStatusbarEntryAccessor; entry: IStatusbarEntry; alignment: MainThreadStatusBarAlignment; priority: number }> = new Map();
private readonly _onDidChange = new Emitter<IExtensionStatusBarItemChangeEvent>();
readonly onDidChange: Event<IExtensionStatusBarItemChangeEvent> = 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 });
}
});
}

View file

@ -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);
},

View file

@ -1174,6 +1174,7 @@ export interface ExtHostChatShape {
$provideWelcomeMessage(handle: number, token: CancellationToken): Promise<(string | IChatReplyFollowup[])[] | undefined>;
$provideFollowups(handle: number, sessionId: number, token: CancellationToken): Promise<IChatFollowup[] | undefined>;
$provideReply(handle: number, sessionId: number, request: IChatRequestDto, token: CancellationToken): Promise<IChatResponseDto | undefined>;
$removeRequest(handle: number, sessionId: number, requestId: string): void;
$provideSlashCommands(handle: number, sessionId: number, token: CancellationToken): Promise<ISlashCommand[] | undefined>;
$releaseSession(sessionId: number): void;
$onDidPerformUserAction(event: IChatUserActionEvent): Promise<void>;
@ -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<void>;
$onFoundNewCandidates(candidates: CandidatePort[]): Promise<void>;
$setCandidatePortSource(source: CandidatePortSource): Promise<void>;
$registerPortsAttributesProvider(selector: PortAttributesProviderSelector, providerHandle: number): Promise<void>;
$registerPortsAttributesProvider(selector: PortAttributesSelector, providerHandle: number): Promise<void>;
$unregisterPortsAttributesProvider(providerHandle: number): Promise<void>;
}
@ -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

View file

@ -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<T> {
@ -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<IChatResponseDto | undefined> {
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;

View file

@ -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<EditorChatApiArg | undefined, InteractiveEditorRunOptions | undefined>('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<IRelaxedExtensionDescription>, provider: vscode.InteractiveEditorSessionProvider): vscode.Disposable {

View file

@ -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);

View file

@ -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 });
}

View file

@ -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;
},

View file

@ -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<vscode.TunnelDescription[]>;
onDidChangeTunnels: vscode.Event<void>;
setTunnelFactory(provider: vscode.RemoteAuthorityResolver | undefined): Promise<IDisposable>;
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>('IExtHostTunnelService');
@ -74,7 +74,7 @@ export class ExtHostTunnelService implements IExtHostTunnelService {
async setTunnelFactory(provider: vscode.RemoteAuthorityResolver | undefined): Promise<IDisposable> {
return { dispose: () => { } };
}
registerPortsAttributesProvider(portSelector: { pid?: number; portRange?: [number, number] }, provider: vscode.PortAttributesProvider) {
registerPortsAttributesProvider(portSelector: PortAttributesSelector, provider: vscode.PortAttributesProvider) {
return { dispose: () => { } };
}

View file

@ -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<number, { provider: vscode.PortAttributesProvider; selector: PortAttributesProviderSelector }> = new Map();
private _portAttributesProviders: Map<number, { provider: vscode.PortAttributesProvider; selector: PortAttributesSelector }> = 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 });

View file

@ -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 };
}

View file

@ -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<string, UriComponents>;
readonly readmeUri?: UriComponents;
readonly changelogUri?: UriComponents;
}
export interface ICommonTelemetryPropertiesResolver {
(): { [key: string]: any };

View file

@ -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<IAction2Options> & { 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<ChatViewPane> {
constructor() {
super(getClearChatActionDescriptorForViewTitle(viewId, providerId));
}
async runInView(accessor: ServicesAccessor, view: ChatViewPane) {
await view.clear();
}
};
}
const getHistoryChatActionDescriptorForViewTitle = (viewId: string, providerId: string): Readonly<IAction2Options> & { viewId: string } => ({
viewId,
id: `workbench.action.chat.${providerId}.history`,

View file

@ -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<void> {
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<void> {
export async function clearChatEditor(accessor: ServicesAccessor): Promise<void> {
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: <IChatEditorOptions>{ 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: <IChatEditorOptions>{ target: { providerId: chatEditorInput.providerId, pinned: true } } }
}], editorGroupsService.activeGroup);
}
}

View file

@ -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<IAction2Options> & { 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<ChatViewPane> {
constructor() {
super(getClearChatActionDescriptorForViewTitle(viewId, providerId));
}
async runInView(accessor: ServicesAccessor, view: ChatViewPane) {
await view.clear();
}
};
}

View file

@ -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<IAction2Options> & { 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<ChatViewPane> {
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: <IChatEditorOptions>{ target: { sessionId: viewModel.sessionId }, pinned: true } });
}
};
}
const getMoveToSidebarChatActionDescriptorForViewTitle = (viewId: string, providerId: string): Readonly<IAction2Options> & { 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<void> {
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: <IChatEditorOptions>{ 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);
}
});
}

View file

@ -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 {

View file

@ -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<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
@ -128,6 +129,7 @@ registerChatTitleActions();
registerChatExecuteActions();
registerChatQuickQuestionActions();
registerChatExportActions();
registerMoveActions();
registerSingleton(IChatService, ChatService, InstantiationType.Delayed);
registerSingleton(IChatContributionService, ChatContributionService, InstantiationType.Delayed);

View file

@ -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<IViewsRegistry>(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<IViewsRegistry>(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, viewContainer);
Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).deregisterViewContainer(viewContainer);
clearAction.dispose();
historyAction.dispose();
openEditor.dispose();
disposables.dispose();
}
};
}

View file

@ -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()));

View file

@ -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<Ch
@ILogService private readonly logService: ILogService,
@ICommandService private readonly commandService: ICommandService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IChatService private readonly chatService: IChatService,
@IChatService private readonly chatService: IChatService
) {
super();
this.renderer = this.instantiationService.createInstance(MarkdownRenderer, {});
@ -192,12 +193,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
const contextKeyService = templateDisposables.add(this.contextKeyService.createScoped(rowContainer));
const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService]));
const titleToolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, header, MenuId.ChatTitle, {
const titleToolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, header, MenuId.ChatMessageTitle, {
menuOptions: {
shouldForwardArgs: true
},
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {
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<Ch
'welcome';
this.traceLayout('renderElement', `${kind}, index=${index}`);
CONTEXT_RESPONSE.bindTo(templateData.contextKeyService).set(isResponseVM(element));
CONTEXT_REQUEST.bindTo(templateData.contextKeyService).set(isRequestVM(element));
CONTEXT_RESPONSE_HAS_PROVIDER_ID.bindTo(templateData.contextKeyService).set(isResponseVM(element) && !!element.providerResponseId);
if (isResponseVM(element)) {
CONTEXT_RESPONSE_VOTE.bindTo(templateData.contextKeyService).set(element.vote === InteractiveSessionVoteDirection.Up ? 'up' : element.vote === InteractiveSessionVoteDirection.Down ? 'down' : '');
@ -515,7 +518,6 @@ export class ChatListDelegate implements IListVirtualDelegate<ChatTreeItem> {
}
export class ChatAccessibilityProvider implements IListAccessibilityProvider<ChatTreeItem> {
getWidgetRole(): AriaRole {
return 'list';
}
@ -534,7 +536,7 @@ export class ChatAccessibilityProvider implements IListAccessibilityProvider<Cha
}
if (isResponseVM(element)) {
return element.response.value;
return this._getLabelWithCodeBlockCount(element);
}
if (isWelcomeVM(element)) {
@ -543,6 +545,18 @@ export class ChatAccessibilityProvider implements IListAccessibilityProvider<Cha
return '';
}
private _getLabelWithCodeBlockCount(element: IChatResponseViewModel): string {
const codeBlockCount = marked.lexer(element.response.value).filter(token => 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 = <IChatCodeBlockActionContext>{
code: data.text,
codeBlockIndex: data.codeBlockIndex,

View file

@ -115,8 +115,17 @@ export class ChatViewPane extends ViewPane implements IChatViewPane {
async clear(): Promise<void> {
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 {

View file

@ -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. */

View file

@ -10,6 +10,9 @@ export const CONTEXT_RESPONSE_HAS_PROVIDER_ID = new RawContextKey<boolean>('chat
export const CONTEXT_RESPONSE_VOTE = new RawContextKey<string>('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<boolean>('chatSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") });
export const CONTEXT_RESPONSE = new RawContextKey<boolean>('chatResponse', false, { type: 'boolean', description: localize('chatResponse', "The chat item is a response.") });
export const CONTEXT_REQUEST = new RawContextKey<boolean>('chatRequest', false, { type: 'boolean', description: localize('chatRequest', "The chat item is a request") });
export const CONTEXT_CHAT_INPUT_HAS_TEXT = new RawContextKey<boolean>('chatInputHasText', false, { type: 'boolean', description: localize('interactiveInputHasText', "True when the chat input has text.") });
export const CONTEXT_IN_CHAT_INPUT = new RawContextKey<boolean>('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<boolean>('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") });

View file

@ -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,

View file

@ -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<IChatFollowup[] | undefined>;
provideReply(request: IChatRequest, progress: (progress: IChatProgress) => void, token: CancellationToken): ProviderResult<IChatResponse>;
provideSlashCommands?(session: IChat, token: CancellationToken): ProviderResult<ISlashCommand[]>;
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<void> } | undefined>;
removeRequest(sessionid: string, requestId: string): Promise<void>;
cancelCurrentRequestForSession(sessionId: string): void;
getSlashCommands(sessionId: string, token: CancellationToken): Promise<ISlashCommand[] | undefined>;
clearSession(sessionId: string): void;

View file

@ -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<void> {
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<string> {
const slashCommands = await this.getSlashCommands(sessionId, CancellationToken.None);
for (const slashCommand of slashCommands ?? []) {

View file

@ -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<void>;
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<void>());
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;
}

View file

@ -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<string>('_workbench.experimentsUrl') || this.productService.experimentsUrl;
const experimentsUrl = this.productService.experimentsUrl;
if (!experimentsUrl || this.configurationService.getValue('workbench.enableExperiments') === false) {
return [];
}

View file

@ -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,

View file

@ -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();
}
}));

View file

@ -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;
}

View file

@ -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<boolean>;
private readonly _ctxShowingDiff: IContextKey<boolean>;
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);
}
}

View file

@ -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);

View file

@ -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

View file

@ -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 {

View file

@ -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);
}
}
}));

View file

@ -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;
}
});
}

View file

@ -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(`<div aria-label='${uriText}'>${uriText}</div>`, { 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 });
}
}
}
});

View file

@ -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;
}

View file

@ -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<URI | undefined> {
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;

View file

@ -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<boolean | undefined>;
provideShare(item: IShareableItem, token: CancellationToken): Thenable<URI | undefined>;

View file

@ -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) {

View file

@ -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<ITerminalContributions> = {

View file

@ -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];

View file

@ -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
*/

View file

@ -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 => ({

View file

@ -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,

View file

@ -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();

View file

@ -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<string, UriComponents>;
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((<any>thing).path) &&
isString((<any>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<string, URI>()) : 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<IScannedExtension> {
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<string, URI>, fallbackPackageNLSUri?: URI | null, readmeUri?: URI, changelogUri?: URI, metadata?: Metadata): Promise<IWebExtension> {
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<string, URI>, fallbackPackageNLSUri?: URI | ITranslations | null, readmeUri?: URI, changelogUri?: URI, metadata?: Metadata): Promise<IWebExtension> {
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,
};

View file

@ -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',

View file

@ -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;

View file

@ -27,6 +27,7 @@ suite('DomActivityTracker', () => {
clock.restore();
});
test('marks inactive on no input', () => {
assert.equal(uas.isActive, true);
clock.tick(maxTimeToBecomeIdle);

View file

@ -151,6 +151,9 @@ declare module 'vscode' {
prepareSession(initialState: InteractiveSessionState | undefined, token: CancellationToken): ProviderResult<S>;
resolveRequest(session: S, context: InteractiveSessionRequestArgs | string, token: CancellationToken): ProviderResult<InteractiveRequest>;
provideResponseWithProgress(request: InteractiveRequest, progress: Progress<InteractiveProgress>, token: CancellationToken): ProviderResult<InteractiveResponseForProgress>;
// eslint-disable-next-line local/vscode-dts-provider-naming
removeRequest(session: S, requestId: string): void;
}
export interface InteractiveSessionDynamicRequest {

View file

@ -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<PortAttributes>;
}
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;
}
}

View file

@ -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;
}
}

Some files were not shown because too many files have changed in this diff Show more