Merge from vscode 073a24de05773f2261f89172987002dc0ae2f1cd (#9711)

This commit is contained in:
Anthony Dresser
2020-03-24 00:24:15 -07:00
committed by GitHub
parent 29741d684e
commit 89ef1b0c2e
226 changed files with 6161 additions and 3288 deletions

View File

@@ -77,6 +77,23 @@ steps:
yarn postinstall yarn postinstall
displayName: Run postinstall scripts displayName: Run postinstall scripts
condition: and(succeeded(), eq(variables['CacheRestored'], 'true')) condition: and(succeeded(), eq(variables['CacheRestored'], 'true'))
env:
OSS_GITHUB_ID: "a5d3c261b032765a78de"
OSS_GITHUB_SECRET: $(oss-github-client-secret)
INSIDERS_GITHUB_ID: "31f02627809389d9f111"
INSIDERS_GITHUB_SECRET: $(insiders-github-client-secret)
STABLE_GITHUB_ID: "baa8a44b5e861d918709"
STABLE_GITHUB_SECRET: $(stable-github-client-secret)
EXPLORATION_GITHUB_ID: "94e8376d3a90429aeaea"
EXPLORATION_GITHUB_SECRET: $(exploration-github-client-secret)
VSO_GITHUB_ID: "3d4be8f37a0325b5817d"
VSO_GITHUB_SECRET: $(vso-github-client-secret)
VSO_PPE_GITHUB_ID: "eabf35024dc2e891a492"
VSO_PPE_GITHUB_SECRET: $(vso-ppe-github-client-secret)
VSO_DEV_GITHUB_ID: "84383ebd8a7c5f5efc5c"
VSO_DEV_GITHUB_SECRET: $(vso-dev-github-client-secret)
GITHUB_APP_ID: "Iv1.ae51e546bef24ff1"
GITHUB_APP_SECRET: $(github-app-client-secret)
- script: | - script: |
set -e set -e

View File

@@ -86,6 +86,23 @@ steps:
yarn postinstall yarn postinstall
displayName: Run postinstall scripts displayName: Run postinstall scripts
condition: and(succeeded(), eq(variables['CacheRestored'], 'true')) condition: and(succeeded(), eq(variables['CacheRestored'], 'true'))
env:
OSS_GITHUB_ID: "a5d3c261b032765a78de"
OSS_GITHUB_SECRET: $(oss-github-client-secret)
INSIDERS_GITHUB_ID: "31f02627809389d9f111"
INSIDERS_GITHUB_SECRET: $(insiders-github-client-secret)
STABLE_GITHUB_ID: "baa8a44b5e861d918709"
STABLE_GITHUB_SECRET: $(stable-github-client-secret)
EXPLORATION_GITHUB_ID: "94e8376d3a90429aeaea"
EXPLORATION_GITHUB_SECRET: $(exploration-github-client-secret)
VSO_GITHUB_ID: "3d4be8f37a0325b5817d"
VSO_GITHUB_SECRET: $(vso-github-client-secret)
VSO_PPE_GITHUB_ID: "eabf35024dc2e891a492"
VSO_PPE_GITHUB_SECRET: $(vso-ppe-github-client-secret)
VSO_DEV_GITHUB_ID: "84383ebd8a7c5f5efc5c"
VSO_DEV_GITHUB_SECRET: $(vso-dev-github-client-secret)
GITHUB_APP_ID: "Iv1.ae51e546bef24ff1"
GITHUB_APP_SECRET: $(github-app-client-secret)
- script: | - script: |
set -e set -e

View File

@@ -76,6 +76,23 @@ steps:
yarn postinstall yarn postinstall
displayName: Run postinstall scripts displayName: Run postinstall scripts
condition: and(succeeded(), eq(variables['CacheRestored'], 'true')) condition: and(succeeded(), eq(variables['CacheRestored'], 'true'))
env:
OSS_GITHUB_ID: "a5d3c261b032765a78de"
OSS_GITHUB_SECRET: $(oss-github-client-secret)
INSIDERS_GITHUB_ID: "31f02627809389d9f111"
INSIDERS_GITHUB_SECRET: $(insiders-github-client-secret)
STABLE_GITHUB_ID: "baa8a44b5e861d918709"
STABLE_GITHUB_SECRET: $(stable-github-client-secret)
EXPLORATION_GITHUB_ID: "94e8376d3a90429aeaea"
EXPLORATION_GITHUB_SECRET: $(exploration-github-client-secret)
VSO_GITHUB_ID: "3d4be8f37a0325b5817d"
VSO_GITHUB_SECRET: $(vso-github-client-secret)
VSO_PPE_GITHUB_ID: "eabf35024dc2e891a492"
VSO_PPE_GITHUB_SECRET: $(vso-ppe-github-client-secret)
VSO_DEV_GITHUB_ID: "84383ebd8a7c5f5efc5c"
VSO_DEV_GITHUB_SECRET: $(vso-dev-github-client-secret)
GITHUB_APP_ID: "Iv1.ae51e546bef24ff1"
GITHUB_APP_SECRET: $(github-app-client-secret)
- script: | - script: |
set -e set -e

View File

@@ -92,6 +92,8 @@ steps:
VSO_PPE_GITHUB_SECRET: $(vso-ppe-github-client-secret) VSO_PPE_GITHUB_SECRET: $(vso-ppe-github-client-secret)
VSO_DEV_GITHUB_ID: "84383ebd8a7c5f5efc5c" VSO_DEV_GITHUB_ID: "84383ebd8a7c5f5efc5c"
VSO_DEV_GITHUB_SECRET: $(vso-dev-github-client-secret) VSO_DEV_GITHUB_SECRET: $(vso-dev-github-client-secret)
GITHUB_APP_ID: "Iv1.ae51e546bef24ff1"
GITHUB_APP_SECRET: $(github-app-client-secret)
# Mixin must run before optimize, because the CSS loader will # Mixin must run before optimize, because the CSS loader will
# inline small SVGs # inline small SVGs

View File

@@ -86,6 +86,23 @@ steps:
exec { yarn postinstall } exec { yarn postinstall }
displayName: Run postinstall scripts displayName: Run postinstall scripts
condition: and(succeeded(), eq(variables['CacheRestored'], 'true')) condition: and(succeeded(), eq(variables['CacheRestored'], 'true'))
env:
OSS_GITHUB_ID: "a5d3c261b032765a78de"
OSS_GITHUB_SECRET: $(oss-github-client-secret)
INSIDERS_GITHUB_ID: "31f02627809389d9f111"
INSIDERS_GITHUB_SECRET: $(insiders-github-client-secret)
STABLE_GITHUB_ID: "baa8a44b5e861d918709"
STABLE_GITHUB_SECRET: $(stable-github-client-secret)
EXPLORATION_GITHUB_ID: "94e8376d3a90429aeaea"
EXPLORATION_GITHUB_SECRET: $(exploration-github-client-secret)
VSO_GITHUB_ID: "3d4be8f37a0325b5817d"
VSO_GITHUB_SECRET: $(vso-github-client-secret)
VSO_PPE_GITHUB_ID: "eabf35024dc2e891a492"
VSO_PPE_GITHUB_SECRET: $(vso-ppe-github-client-secret)
VSO_DEV_GITHUB_ID: "84383ebd8a7c5f5efc5c"
VSO_DEV_GITHUB_SECRET: $(vso-dev-github-client-secret)
GITHUB_APP_ID: "Iv1.ae51e546bef24ff1"
GITHUB_APP_SECRET: $(github-app-client-secret)
- powershell: | - powershell: |
. build/azure-pipelines/win32/exec.ps1 . build/azure-pipelines/win32/exec.ps1

View File

@@ -349,6 +349,10 @@
{ {
"name": "vs/workbench/contrib/timeline", "name": "vs/workbench/contrib/timeline",
"project": "vscode-workbench" "project": "vscode-workbench"
},
{
"name": "vs/workbench/services/authentication",
"project": "vscode-workbench"
} }
] ]
} }

View File

@@ -23,7 +23,7 @@ if (majorYarnVersion < 1 || minorYarnVersion < 10) {
err = true; err = true;
} }
if (!/yarn\.js$|yarnpkg$/.test(process.env['npm_execpath'])) { if (!/yarn[\w-.]*\.js$|yarnpkg$/.test(process.env['npm_execpath'])) {
console.error('\033[1;31m*** Please use yarn to install dependencies.\033[0;0m'); console.error('\033[1;31m*** Please use yarn to install dependencies.\033[0;0m');
err = true; err = true;
} }

View File

@@ -96,6 +96,10 @@
"fileMatch": "%APP_SETTINGS_HOME%/snippets/*.json", "fileMatch": "%APP_SETTINGS_HOME%/snippets/*.json",
"url": "vscode://schemas/snippets" "url": "vscode://schemas/snippets"
}, },
{
"fileMatch": "%APP_SETTINGS_HOME%/sync/snippets/preview/*.json",
"url": "vscode://schemas/snippets"
},
{ {
"fileMatch": "**/*.code-snippets", "fileMatch": "**/*.code-snippets",
"url": "vscode://schemas/global-snippets" "url": "vscode://schemas/global-snippets"

View File

@@ -2360,7 +2360,13 @@ export class CommandCenter {
title = localize('git.title.diffRefs', '{0} ({1}) ⟷ {0} ({2})', basename, item.shortPreviousRef, item.shortRef); title = localize('git.title.diffRefs', '{0} ({1}) ⟷ {0} ({2})', basename, item.shortPreviousRef, item.shortRef);
} }
return commands.executeCommand('vscode.diff', toGitUri(uri, item.previousRef), item.ref === '' ? uri : toGitUri(uri, item.ref), title); const options: TextDocumentShowOptions = {
preserveFocus: true,
preview: true,
viewColumn: ViewColumn.Active
};
return commands.executeCommand('vscode.diff', toGitUri(uri, item.previousRef), item.ref === '' ? uri : toGitUri(uri, item.ref), title, options);
} }
@command('git.timeline.copyCommitId', { repository: false }) @command('git.timeline.copyCommitId', { repository: false })

View File

@@ -45,7 +45,7 @@ interface MutableRemote extends Remote {
isReadOnly: boolean; isReadOnly: boolean;
} }
// TODO[ECA]: Move to git.d.ts once we are good with the api // TODO@eamodio: Move to git.d.ts once we are good with the api
/** /**
* Log file options. * Log file options.
*/ */

View File

@@ -15,7 +15,7 @@ dayjs.extend(advancedFormat);
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
// TODO[ECA]: Localize or use a setting for date format // TODO@eamodio: Localize or use a setting for date format
export class GitTimelineItem extends TimelineItem { export class GitTimelineItem extends TimelineItem {
static is(item: TimelineItem): item is GitTimelineItem { static is(item: TimelineItem): item is GitTimelineItem {
@@ -71,21 +71,21 @@ export class GitTimelineProvider implements TimelineProvider {
readonly id = 'git-history'; readonly id = 'git-history';
readonly label = localize('git.timeline.source', 'Git History'); readonly label = localize('git.timeline.source', 'Git History');
private _disposable: Disposable; private disposable: Disposable;
private _repo: Repository | undefined; private repo: Repository | undefined;
private _repoDisposable: Disposable | undefined; private repoDisposable: Disposable | undefined;
private _repoStatusDate: Date | undefined; private repoStatusDate: Date | undefined;
constructor(private readonly _model: Model) { constructor(private readonly _model: Model) {
this._disposable = Disposable.from( this.disposable = Disposable.from(
_model.onDidOpenRepository(this.onRepositoriesChanged, this), _model.onDidOpenRepository(this.onRepositoriesChanged, this),
workspace.registerTimelineProvider(['file', 'git', 'gitlens-git'], this), workspace.registerTimelineProvider(['file', 'git', 'gitlens-git'], this),
); );
} }
dispose() { dispose() {
this._disposable.dispose(); this.disposable.dispose();
} }
async provideTimeline(uri: Uri, options: TimelineOptions, _token: CancellationToken): Promise<Timeline> { async provideTimeline(uri: Uri, options: TimelineOptions, _token: CancellationToken): Promise<Timeline> {
@@ -93,33 +93,33 @@ export class GitTimelineProvider implements TimelineProvider {
const repo = this._model.getRepository(uri); const repo = this._model.getRepository(uri);
if (!repo) { if (!repo) {
this._repoDisposable?.dispose(); this.repoDisposable?.dispose();
this._repoStatusDate = undefined; this.repoStatusDate = undefined;
this._repo = undefined; this.repo = undefined;
return { items: [] }; return { items: [] };
} }
if (this._repo?.root !== repo.root) { if (this.repo?.root !== repo.root) {
this._repoDisposable?.dispose(); this.repoDisposable?.dispose();
this._repo = repo; this.repo = repo;
this._repoStatusDate = new Date(); this.repoStatusDate = new Date();
this._repoDisposable = Disposable.from( this.repoDisposable = Disposable.from(
repo.onDidChangeRepository(uri => this.onRepositoryChanged(repo, uri)), repo.onDidChangeRepository(uri => this.onRepositoryChanged(repo, uri)),
repo.onDidRunGitStatus(() => this.onRepositoryStatusChanged(repo)) repo.onDidRunGitStatus(() => this.onRepositoryStatusChanged(repo))
); );
} }
// TODO[ECA]: Ensure that the uri is a file -- if not we could get the history of the repo? // TODO@eamodio: Ensure that the uri is a file -- if not we could get the history of the repo?
let limit: number | undefined; let limit: number | undefined;
if (options.limit !== undefined && typeof options.limit !== 'number') { if (options.limit !== undefined && typeof options.limit !== 'number') {
try { try {
const result = await this._model.git.exec(repo.root, ['rev-list', '--count', `${options.limit.cursor}..`, '--', uri.fsPath]); const result = await this._model.git.exec(repo.root, ['rev-list', '--count', `${options.limit.id}..`, '--', uri.fsPath]);
if (!result.exitCode) { if (!result.exitCode) {
// Ask for 1 more than so we can determine if there are more commits // Ask for 2 more (1 for the limit commit and 1 for the next commit) than so we can determine if there are more commits
limit = Number(result.stdout) + 1; limit = Number(result.stdout) + 2;
} }
} }
catch { catch {
@@ -130,21 +130,14 @@ export class GitTimelineProvider implements TimelineProvider {
limit = options.limit === undefined ? undefined : options.limit + 1; limit = options.limit === undefined ? undefined : options.limit + 1;
} }
const commits = await repo.logFile(uri, { const commits = await repo.logFile(uri, {
maxEntries: limit, maxEntries: limit,
hash: options.cursor, hash: options.cursor,
reverse: options.before,
// sortByAuthorDate: true // sortByAuthorDate: true
}); });
const more = limit === undefined || options.before ? false : commits.length >= limit;
const paging = commits.length ? { const paging = commits.length ? {
more: more, cursor: limit === undefined ? undefined : (commits.length >= limit ? commits[commits.length - 1]?.hash : undefined)
cursors: {
before: commits[0]?.hash,
after: commits[commits.length - (more ? 1 : 2)]?.hash
}
} : undefined; } : undefined;
// If we asked for an extra commit, strip it off // If we asked for an extra commit, strip it off
@@ -153,12 +146,12 @@ export class GitTimelineProvider implements TimelineProvider {
} }
let dateFormatter: dayjs.Dayjs; let dateFormatter: dayjs.Dayjs;
const items = commits.map<GitTimelineItem>(c => { const items = commits.map<GitTimelineItem>((c, i) => {
const date = c.commitDate; // c.authorDate const date = c.commitDate; // c.authorDate
dateFormatter = dayjs(date); dateFormatter = dayjs(date);
const item = new GitTimelineItem(c.hash, `${c.hash}^`, c.message, date?.getTime() ?? 0, c.hash, 'git:file:commit'); const item = new GitTimelineItem(c.hash, commits[i + 1]?.hash ?? `${c.hash}^`, c.message, date?.getTime() ?? 0, c.hash, 'git:file:commit');
item.iconPath = new (ThemeIcon as any)('git-commit'); item.iconPath = new (ThemeIcon as any)('git-commit');
item.description = c.authorName; item.description = c.authorName;
item.detail = `${c.authorName} (${c.authorEmail}) \u2014 ${c.hash.substr(0, 8)}\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n\n${c.message}`; item.detail = `${c.authorName} (${c.authorEmail}) \u2014 ${c.hash.substr(0, 8)}\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n\n${c.message}`;
@@ -171,16 +164,16 @@ export class GitTimelineProvider implements TimelineProvider {
return item; return item;
}); });
if (options.cursor === undefined || options.before) { if (options.cursor === undefined) {
const you = localize('git.timeline.you', 'You'); const you = localize('git.timeline.you', 'You');
const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath); const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
if (index) { if (index) {
const date = this._repoStatusDate ?? new Date(); const date = this.repoStatusDate ?? new Date();
dateFormatter = dayjs(date); dateFormatter = dayjs(date);
const item = new GitTimelineItem('~', 'HEAD', localize('git.timeline.stagedChanges', 'Staged Changes'), date.getTime(), 'index', 'git:file:index'); const item = new GitTimelineItem('~', 'HEAD', localize('git.timeline.stagedChanges', 'Staged Changes'), date.getTime(), 'index', 'git:file:index');
// TODO[ECA]: Replace with a better icon -- reflecting its status maybe? // TODO@eamodio: Replace with a better icon -- reflecting its status maybe?
item.iconPath = new (ThemeIcon as any)('git-commit'); item.iconPath = new (ThemeIcon as any)('git-commit');
item.description = ''; item.description = '';
item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.index', 'Index'), dateFormatter.format('MMMM Do, YYYY h:mma'), Resource.getStatusText(index.type)); item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.index', 'Index'), dateFormatter.format('MMMM Do, YYYY h:mma'), Resource.getStatusText(index.type));
@@ -199,7 +192,7 @@ export class GitTimelineProvider implements TimelineProvider {
dateFormatter = dayjs(date); dateFormatter = dayjs(date);
const item = new GitTimelineItem('', index ? '~' : 'HEAD', localize('git.timeline.uncommitedChanges', 'Uncommited Changes'), date.getTime(), 'working', 'git:file:working'); const item = new GitTimelineItem('', index ? '~' : 'HEAD', localize('git.timeline.uncommitedChanges', 'Uncommited Changes'), date.getTime(), 'working', 'git:file:working');
// TODO[ECA]: Replace with a better icon -- reflecting its status maybe? // TODO@eamodio: Replace with a better icon -- reflecting its status maybe?
item.iconPath = new (ThemeIcon as any)('git-commit'); item.iconPath = new (ThemeIcon as any)('git-commit');
item.description = ''; item.description = '';
item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.workingTree', 'Working Tree'), dateFormatter.format('MMMM Do, YYYY h:mma'), Resource.getStatusText(working.type)); item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.workingTree', 'Working Tree'), dateFormatter.format('MMMM Do, YYYY h:mma'), Resource.getStatusText(working.type));
@@ -222,7 +215,7 @@ export class GitTimelineProvider implements TimelineProvider {
private onRepositoriesChanged(_repo: Repository) { private onRepositoriesChanged(_repo: Repository) {
// console.log(`GitTimelineProvider.onRepositoriesChanged`); // console.log(`GitTimelineProvider.onRepositoriesChanged`);
// TODO[ECA]: Being naive for now and just always refreshing each time there is a new repository // TODO@eamodio: Being naive for now and just always refreshing each time there is a new repository
this.fireChanged(); this.fireChanged();
} }
@@ -236,7 +229,7 @@ export class GitTimelineProvider implements TimelineProvider {
// console.log(`GitTimelineProvider.onRepositoryStatusChanged`); // console.log(`GitTimelineProvider.onRepositoryStatusChanged`);
// This is crappy, but for now just save the last time a status was run and use that as the timestamp for staged items // This is crappy, but for now just save the last time a status was run and use that as the timestamp for staged items
this._repoStatusDate = new Date(); this.repoStatusDate = new Date();
this.fireChanged(); this.fireChanged();
} }

View File

@@ -20,7 +20,16 @@ function main() {
} }
} }
fs.writeFileSync(path.join(__dirname, '../src/common/config.json'), JSON.stringify(content)); const githubAppId = process.env.GITHUB_APP_ID;
const githubAppSecret = process.env.GITHUB_APP_SECRET;
if (githubAppId && githubAppSecret) {
content.GITHUB_APP = { id: githubAppId, secret: githubAppSecret }
}
if (Object.keys(content).length > 0) {
fs.writeFileSync(path.join(__dirname, '../src/common/config.json'), JSON.stringify(content));
}
} }
main(); main();

View File

@@ -3,7 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { Uri } from 'vscode'; import { Uri, env } from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
export interface ClientDetails { export interface ClientDetails {
id?: string; id?: string;
@@ -19,6 +21,8 @@ export interface ClientConfig {
VSO: ClientDetails; VSO: ClientDetails;
VSO_PPE: ClientDetails; VSO_PPE: ClientDetails;
VSO_DEV: ClientDetails; VSO_DEV: ClientDetails;
GITHUB_APP: ClientDetails;
} }
export class Registrar { export class Registrar {
@@ -26,7 +30,8 @@ export class Registrar {
constructor() { constructor() {
try { try {
this._config = require('./config.json') as ClientConfig; const fileContents = fs.readFileSync(path.join(env.appRoot, 'extensions/github-authentication/src/common/config.json')).toString();
this._config = JSON.parse(fileContents);
} catch (e) { } catch (e) {
this._config = { this._config = {
OSS: {}, OSS: {},
@@ -35,10 +40,20 @@ export class Registrar {
EXPLORATION: {}, EXPLORATION: {},
VSO: {}, VSO: {},
VSO_PPE: {}, VSO_PPE: {},
VSO_DEV: {} VSO_DEV: {},
GITHUB_APP: {}
}; };
} }
} }
getGitHubAppDetails(): ClientDetails {
if (!this._config.GITHUB_APP.id || !this._config.GITHUB_APP.secret) {
throw new Error(`No GitHub App client configuration available`);
}
return this._config.GITHUB_APP;
}
getClientDetails(callbackUri: Uri): ClientDetails { getClientDetails(callbackUri: Uri): ClientDetails {
let details: ClientDetails | undefined; let details: ClientDetails | undefined;
switch (callbackUri.scheme) { switch (callbackUri.scheme) {

View File

@@ -20,9 +20,9 @@ export async function activate(context: vscode.ExtensionContext) {
displayName: 'GitHub', displayName: 'GitHub',
onDidChangeSessions: onDidChangeSessions.event, onDidChangeSessions: onDidChangeSessions.event,
getSessions: () => Promise.resolve(loginService.sessions), getSessions: () => Promise.resolve(loginService.sessions),
login: async (scopes: string[]) => { login: async (scopeList: string[]) => {
try { try {
const session = await loginService.login(scopes.join(' ')); const session = await loginService.login(scopeList.join(' '));
Logger.info('Login success!'); Logger.info('Login success!');
return session; return session;
} catch (e) { } catch (e) {

View File

@@ -8,7 +8,7 @@ import { keychain } from './common/keychain';
import { GitHubServer } from './githubServer'; import { GitHubServer } from './githubServer';
import Logger from './common/logger'; import Logger from './common/logger';
export const onDidChangeSessions = new vscode.EventEmitter<void>(); export const onDidChangeSessions = new vscode.EventEmitter<vscode.AuthenticationSessionsChangeEvent>();
interface SessionData { interface SessionData {
id: string; id: string;
@@ -29,14 +29,16 @@ export class GitHubAuthenticationProvider {
private pollForChange() { private pollForChange() {
setTimeout(async () => { setTimeout(async () => {
const storedSessions = await this.readSessions(); const storedSessions = await this.readSessions();
let didChange = false;
const added: string[] = [];
const removed: string[] = [];
storedSessions.forEach(session => { storedSessions.forEach(session => {
const matchesExisting = this._sessions.some(s => s.id === session.id); const matchesExisting = this._sessions.some(s => s.id === session.id);
// Another window added a session to the keychain, add it to our state as well // Another window added a session to the keychain, add it to our state as well
if (!matchesExisting) { if (!matchesExisting) {
this._sessions.push(session); this._sessions.push(session);
didChange = true; added.push(session.id);
} }
}); });
@@ -49,12 +51,12 @@ export class GitHubAuthenticationProvider {
this._sessions.splice(sessionIndex, 1); this._sessions.splice(sessionIndex, 1);
} }
didChange = true; removed.push(session.id);
} }
}); });
if (didChange) { if (added.length || removed.length) {
onDidChangeSessions.fire(); onDidChangeSessions.fire({ added, removed, changed: [] });
} }
this.pollForChange(); this.pollForChange();
@@ -101,12 +103,22 @@ export class GitHubAuthenticationProvider {
} }
public async login(scopes: string): Promise<vscode.AuthenticationSession> { public async login(scopes: string): Promise<vscode.AuthenticationSession> {
const token = await this._githubServer.login(scopes); const token = scopes === 'vso' ? await this.loginAndInstallApp(scopes) : await this._githubServer.login(scopes);
const session = await this.tokenToSession(token, scopes.split(' ')); const session = await this.tokenToSession(token, scopes.split(' '));
await this.setToken(session); await this.setToken(session);
return session; return session;
} }
public async loginAndInstallApp(scopes: string): Promise<string> {
const token = await this._githubServer.login(scopes);
const hasUserInstallation = await this._githubServer.hasUserInstallation(token);
if (hasUserInstallation) {
return token;
} else {
return this._githubServer.installApp();
}
}
private async tokenToSession(token: string, scopes: string[]): Promise<vscode.AuthenticationSession> { private async tokenToSession(token: string, scopes: string[]): Promise<vscode.AuthenticationSession> {
const userInfo = await this._githubServer.getUserInfo(token); const userInfo = await this._githubServer.getUserInfo(token);
return { return {

View File

@@ -71,13 +71,58 @@ export class GitHubServer {
Logger.info('Logging in...'); Logger.info('Logging in...');
const state = uuid(); const state = uuid();
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`)); const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`));
const clientDetails = ClientRegistrar.getClientDetails(callbackUri); const clientDetails = scopes === 'vso' ? ClientRegistrar.getGitHubAppDetails() : ClientRegistrar.getClientDetails(callbackUri);
const uri = vscode.Uri.parse(`https://github.com/login/oauth/authorize?redirect_uri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&client_id=${clientDetails.id}`); const uri = vscode.Uri.parse(`https://github.com/login/oauth/authorize?redirect_uri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&client_id=${clientDetails.id}`);
vscode.env.openExternal(uri); vscode.env.openExternal(uri);
return promiseFromEvent(uriHandler.event, exchangeCodeForToken(state, clientDetails)); return promiseFromEvent(uriHandler.event, exchangeCodeForToken(state, clientDetails));
} }
public async hasUserInstallation(token: string): Promise<boolean> {
return new Promise((resolve, reject) => {
Logger.info('Getting user installations...');
const post = https.request({
host: 'api.github.com',
path: `/user/installations`,
method: 'GET',
headers: {
Accept: 'application/vnd.github.machine-man-preview+json',
Authorization: `token ${token}`,
'User-Agent': 'Visual-Studio-Code'
}
}, result => {
const buffer: Buffer[] = [];
result.on('data', (chunk: Buffer) => {
buffer.push(chunk);
});
result.on('end', () => {
if (result.statusCode === 200) {
const json = JSON.parse(Buffer.concat(buffer).toString());
Logger.info('Got installation info!');
const hasInstallation = json.installations.some((installation: { app_slug: string }) => installation.app_slug === 'microsoft-visual-studio-code');
resolve(hasInstallation);
} else {
reject(new Error(result.statusMessage));
}
});
});
post.end();
post.on('error', err => {
reject(err);
});
});
}
public async installApp(): Promise<string> {
const clientDetails = ClientRegistrar.getGitHubAppDetails();
const state = uuid();
const uri = vscode.Uri.parse(`https://github.com/apps/microsoft-visual-studio-code/installations/new?state=${state}`);
vscode.env.openExternal(uri);
return promiseFromEvent(uriHandler.event, exchangeCodeForToken(state, clientDetails));
}
public async getUserInfo(token: string): Promise<{ id: string, accountName: string }> { public async getUserInfo(token: string): Promise<{ id: string, accountName: string }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Logger.info('Getting account info...'); Logger.info('Getting account info...');

View File

@@ -27,8 +27,8 @@ export class PreviewManager implements vscode.CustomEditorProvider {
private readonly zoomStatusBarEntry: ZoomStatusBarEntry, private readonly zoomStatusBarEntry: ZoomStatusBarEntry,
) { } ) { }
public async resolveCustomDocument(_document: vscode.CustomDocument): Promise<void> { public async openCustomDocument(uri: vscode.Uri) {
// noop return new vscode.CustomDocument(PreviewManager.viewType, uri);
} }
public async resolveCustomEditor( public async resolveCustomEditor(

View File

@@ -63,6 +63,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
private _activePreview: DynamicMarkdownPreview | undefined = undefined; private _activePreview: DynamicMarkdownPreview | undefined = undefined;
private readonly customEditorViewType = 'vscode.markdown.preview.editor';
public constructor( public constructor(
private readonly _contentProvider: MarkdownContentProvider, private readonly _contentProvider: MarkdownContentProvider,
private readonly _logger: Logger, private readonly _logger: Logger,
@@ -70,7 +72,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
) { ) {
super(); super();
this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this)); this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this));
this._register(vscode.window.registerCustomEditorProvider('vscode.markdown.preview.editor', this)); this._register(vscode.window.registerCustomEditorProvider(this.customEditorViewType, this));
} }
public refresh() { public refresh() {
@@ -148,8 +150,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
this.registerDynamicPreview(preview); this.registerDynamicPreview(preview);
} }
public async resolveCustomDocument(_document: vscode.CustomDocument): Promise<void> { public async openCustomDocument(uri: vscode.Uri) {
// noop return new vscode.CustomDocument(this.customEditorViewType, uri);
} }
public async resolveCustomTextEditor( public async resolveCustomTextEditor(

View File

@@ -48,12 +48,12 @@ export function loadDefaultTelemetryReporter(): TelemetryReporter {
} }
function getPackageInfo(): IPackageInfo | null { function getPackageInfo(): IPackageInfo | null {
const extention = vscode.extensions.getExtension('Microsoft.vscode-markdown'); const extension = vscode.extensions.getExtension('Microsoft.vscode-markdown');
if (extention && extention.packageJSON) { if (extension && extension.packageJSON) {
return { return {
name: extention.packageJSON.name, name: extension.packageJSON.name,
version: extention.packageJSON.version, version: extension.packageJSON.version,
aiKey: extention.packageJSON.aiKey aiKey: extension.packageJSON.aiKey
}; };
} }
return null; return null;

View File

@@ -54,7 +54,7 @@ function parseQuery(uri: vscode.Uri) {
}, {}); }, {});
} }
export const onDidChangeSessions = new vscode.EventEmitter<void>(); export const onDidChangeSessions = new vscode.EventEmitter<vscode.AuthenticationSessionsChangeEvent>();
export const REFRESH_NETWORK_FAILURE = 'Network failure'; export const REFRESH_NETWORK_FAILURE = 'Network failure';
@@ -129,7 +129,8 @@ export class AzureActiveDirectoryService {
private pollForChange() { private pollForChange() {
setTimeout(async () => { setTimeout(async () => {
let didChange = false; const addedIds: string[] = [];
let removedIds: string[] = [];
const storedData = await keychain.getToken(); const storedData = await keychain.getToken();
if (storedData) { if (storedData) {
try { try {
@@ -139,7 +140,7 @@ export class AzureActiveDirectoryService {
if (!matchesExisting) { if (!matchesExisting) {
try { try {
await this.refreshToken(session.refreshToken, session.scope); await this.refreshToken(session.refreshToken, session.scope);
didChange = true; addedIds.push(session.id);
} catch (e) { } catch (e) {
if (e.message === REFRESH_NETWORK_FAILURE) { if (e.message === REFRESH_NETWORK_FAILURE) {
// Ignore, will automatically retry on next poll. // Ignore, will automatically retry on next poll.
@@ -154,7 +155,7 @@ export class AzureActiveDirectoryService {
const matchesExisting = sessions.some(session => token.scope === session.scope && token.sessionId === session.id); const matchesExisting = sessions.some(session => token.scope === session.scope && token.sessionId === session.id);
if (!matchesExisting) { if (!matchesExisting) {
await this.logout(token.sessionId); await this.logout(token.sessionId);
didChange = true; removedIds.push(token.sessionId);
} }
})); }));
@@ -162,19 +163,19 @@ export class AzureActiveDirectoryService {
} catch (e) { } catch (e) {
Logger.error(e.message); Logger.error(e.message);
// if data is improperly formatted, remove all of it and send change event // if data is improperly formatted, remove all of it and send change event
removedIds = this._tokens.map(token => token.sessionId);
this.clearSessions(); this.clearSessions();
didChange = true;
} }
} else { } else {
if (this._tokens.length) { if (this._tokens.length) {
// Log out all // Log out all
removedIds = this._tokens.map(token => token.sessionId);
await this.clearSessions(); await this.clearSessions();
didChange = true;
} }
} }
if (didChange) { if (addedIds.length || removedIds.length) {
onDidChangeSessions.fire(); onDidChangeSessions.fire({ added: addedIds, removed: removedIds, changed: [] });
} }
this.pollForChange(); this.pollForChange();
@@ -377,7 +378,7 @@ export class AzureActiveDirectoryService {
this._refreshTimeouts.set(token.sessionId, setTimeout(async () => { this._refreshTimeouts.set(token.sessionId, setTimeout(async () => {
try { try {
await this.refreshToken(token.refreshToken, scope); await this.refreshToken(token.refreshToken, scope);
onDidChangeSessions.fire(); onDidChangeSessions.fire({ added: [], removed: [], changed: [token.sessionId] });
} catch (e) { } catch (e) {
if (e.message === REFRESH_NETWORK_FAILURE) { if (e.message === REFRESH_NETWORK_FAILURE) {
const didSucceedOnRetry = await this.handleRefreshNetworkError(token.sessionId, token.refreshToken, scope); const didSucceedOnRetry = await this.handleRefreshNetworkError(token.sessionId, token.refreshToken, scope);
@@ -386,7 +387,7 @@ export class AzureActiveDirectoryService {
} }
} else { } else {
await this.logout(token.sessionId); await this.logout(token.sessionId);
onDidChangeSessions.fire(); onDidChangeSessions.fire({ added: [], removed: [token.sessionId], changed: [] });
} }
} }
}, 1000 * (parseInt(token.expiresIn) - 30))); }, 1000 * (parseInt(token.expiresIn) - 30)));
@@ -548,9 +549,8 @@ export class AzureActiveDirectoryService {
const token = this._tokens.find(token => token.sessionId === sessionId); const token = this._tokens.find(token => token.sessionId === sessionId);
if (token) { if (token) {
token.accessToken = undefined; token.accessToken = undefined;
onDidChangeSessions.fire({ added: [], removed: [], changed: [token.sessionId] });
} }
onDidChangeSessions.fire();
} }
const delayBeforeRetry = 5 * attempts * attempts; const delayBeforeRetry = 5 * attempts * attempts;

View File

@@ -25,13 +25,17 @@ export async function activate(context: vscode.ExtensionContext) {
login: async (scopes: string[]) => { login: async (scopes: string[]) => {
try { try {
await loginService.login(scopes.sort().join(' ')); await loginService.login(scopes.sort().join(' '));
const session = loginService.sessions[loginService.sessions.length - 1];
onDidChangeSessions.fire({ added: [session.id], removed: [], changed: [] });
return loginService.sessions[0]!; return loginService.sessions[0]!;
} catch (e) { } catch (e) {
throw e; throw e;
} }
}, },
logout: async (id: string) => { logout: async (id: string) => {
return loginService.logout(id); await loginService.logout(id);
onDidChangeSessions.fire({ added: [], removed: [id], changed: [] });
vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out."));
} }
})); }));
@@ -46,8 +50,9 @@ export async function activate(context: vscode.ExtensionContext) {
} }
if (sessions.length === 1) { if (sessions.length === 1) {
await loginService.logout(loginService.sessions[0].id); const id = loginService.sessions[0].id;
onDidChangeSessions.fire(); await loginService.logout(id);
onDidChangeSessions.fire({ added: [], removed: [id], changed: [] });
vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out.")); vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out."));
return; return;
} }
@@ -61,7 +66,7 @@ export async function activate(context: vscode.ExtensionContext) {
if (selectedSession) { if (selectedSession) {
await loginService.logout(selectedSession.id); await loginService.logout(selectedSession.id);
onDidChangeSessions.fire(); onDidChangeSessions.fire({ added: [], removed: [selectedSession.id], changed: [] });
vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out.")); vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out."));
return; return;
} }

View File

@@ -184,7 +184,7 @@
"vinyl": "^2.0.0", "vinyl": "^2.0.0",
"vinyl-fs": "^3.0.0", "vinyl-fs": "^3.0.0",
"vsce": "1.48.0", "vsce": "1.48.0",
"vscode-debugprotocol": "1.39.0", "vscode-debugprotocol": "1.40.0-pre.1",
"vscode-nls-dev": "^3.3.1", "vscode-nls-dev": "^3.3.1",
"webpack": "^4.16.5", "webpack": "^4.16.5",
"webpack-cli": "^3.3.8", "webpack-cli": "^3.3.8",

View File

@@ -65,7 +65,7 @@ export interface IIdentityProvider<T> {
export enum ListAriaRootRole { export enum ListAriaRootRole {
/** default list structure role */ /** default list structure role */
LIST = 'listbox', LIST = 'list',
/** default tree structure role */ /** default tree structure role */
TREE = 'tree', TREE = 'tree',

View File

@@ -182,17 +182,19 @@ class Trait<T> implements ISpliceable<boolean>, IDisposable {
class SelectionTrait<T> extends Trait<T> { class SelectionTrait<T> extends Trait<T> {
constructor() { constructor(private setAriaSelected: boolean) {
super('selected'); super('selected');
} }
renderIndex(index: number, container: HTMLElement): void { renderIndex(index: number, container: HTMLElement): void {
super.renderIndex(index, container); super.renderIndex(index, container);
if (this.contains(index)) { if (this.setAriaSelected) {
container.setAttribute('aria-selected', 'true'); if (this.contains(index)) {
} else { container.setAttribute('aria-selected', 'true');
container.setAttribute('aria-selected', 'false'); } else {
container.setAttribute('aria-selected', 'false');
}
} }
} }
} }
@@ -1198,7 +1200,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
renderers: IListRenderer<any /* TODO@joao */, any>[], renderers: IListRenderer<any /* TODO@joao */, any>[],
private _options: IListOptions<T> = DefaultOptions private _options: IListOptions<T> = DefaultOptions
) { ) {
this.selection = new SelectionTrait(); this.selection = new SelectionTrait(this._options.ariaRole !== 'listbox');
this.focus = new Trait('focused'); this.focus = new Trait('focused');
mixin(_options, defaultStyles, false); mixin(_options, defaultStyles, false);
@@ -1501,9 +1503,13 @@ export class List<T> implements ISpliceable<T>, IDisposable {
} }
focusFirst(browserEvent?: UIEvent, filter?: (element: T) => boolean): void { focusFirst(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
this.focusNth(0, browserEvent, filter);
}
focusNth(n: number, browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
if (this.length === 0) { return; } if (this.length === 0) { return; }
const index = this.findNextIndex(0, false, filter); const index = this.findNextIndex(n, false, filter);
if (index > -1) { if (index > -1) {
this.setFocus([index], browserEvent); this.setFocus([index], browserEvent);

View File

@@ -357,7 +357,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.option-disabled:hover { background-color: ${this.styles.selectBackground} !important; }`); content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.option-disabled:hover { background-color: ${this.styles.selectBackground} !important; }`);
} }
// Match quickOpen outline styles - ignore for disabled options // Match quick input outline styles - ignore for disabled options
if (this.styles.listFocusOutline) { if (this.styles.listFocusOutline) {
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { outline: 1.6px dotted ${this.styles.listFocusOutline} !important; outline-offset: -1.6px !important; }`); content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { outline: 1.6px dotted ${this.styles.listFocusOutline} !important; outline-offset: -1.6px !important; }`);
} }

View File

@@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event'; import { domEvent } from 'vs/base/browser/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes'; import { KeyCode } from 'vs/base/common/keyCodes';
import { $, append, addClass, removeClass, toggleClass, trackFocus, EventHelper } from 'vs/base/browser/dom'; import { $, append, addClass, removeClass, toggleClass, trackFocus, EventHelper, clearNode } from 'vs/base/browser/dom';
import { firstIndex } from 'vs/base/common/arrays'; import { firstIndex } from 'vs/base/common/arrays';
import { Color, RGBA } from 'vs/base/common/color'; import { Color, RGBA } from 'vs/base/common/color';
import { SplitView, IView } from './splitview'; import { SplitView, IView } from './splitview';
@@ -52,7 +52,6 @@ export abstract class Pane extends Disposable implements IView {
protected _expanded: boolean; protected _expanded: boolean;
protected _orientation: Orientation; protected _orientation: Orientation;
protected _preventCollapse?: boolean;
private expandedSize: number | undefined = undefined; private expandedSize: number | undefined = undefined;
private _headerVisible = true; private _headerVisible = true;
@@ -106,7 +105,7 @@ export abstract class Pane extends Disposable implements IView {
get minimumSize(): number { get minimumSize(): number {
const headerSize = this.headerSize; const headerSize = this.headerSize;
const expanded = !this.headerVisible || this.isExpanded(); const expanded = !this.headerVisible || this.isExpanded();
const minimumBodySize = expanded ? this._minimumBodySize : 0; const minimumBodySize = expanded ? this._minimumBodySize : this._orientation === Orientation.HORIZONTAL ? 50 : 0;
return headerSize + minimumBodySize; return headerSize + minimumBodySize;
} }
@@ -114,7 +113,7 @@ export abstract class Pane extends Disposable implements IView {
get maximumSize(): number { get maximumSize(): number {
const headerSize = this.headerSize; const headerSize = this.headerSize;
const expanded = !this.headerVisible || this.isExpanded(); const expanded = !this.headerVisible || this.isExpanded();
const maximumBodySize = expanded ? this._maximumBodySize : 0; const maximumBodySize = expanded ? this._maximumBodySize : this._orientation === Orientation.HORIZONTAL ? 50 : 0;
return headerSize + maximumBodySize; return headerSize + maximumBodySize;
} }
@@ -174,6 +173,18 @@ export abstract class Pane extends Disposable implements IView {
this._onDidChange.fire(undefined); this._onDidChange.fire(undefined);
} }
get orientation(): Orientation {
return this._orientation;
}
set orientation(orientation: Orientation) {
if (this._orientation === orientation) {
return;
}
this._orientation = orientation;
}
render(): void { render(): void {
this.header = $('.pane-header'); this.header = $('.pane-header');
append(this.element, this.header); append(this.element, this.header);
@@ -190,22 +201,20 @@ export abstract class Pane extends Disposable implements IView {
this.updateHeader(); this.updateHeader();
if (!this._preventCollapse) { const onHeaderKeyDown = Event.chain(domEvent(this.header, 'keydown'))
const onHeaderKeyDown = Event.chain(domEvent(this.header, 'keydown')) .map(e => new StandardKeyboardEvent(e));
.map(e => new StandardKeyboardEvent(e));
this._register(onHeaderKeyDown.filter(e => e.keyCode === KeyCode.Enter || e.keyCode === KeyCode.Space) this._register(onHeaderKeyDown.filter(e => e.keyCode === KeyCode.Enter || e.keyCode === KeyCode.Space)
.event(() => this.setExpanded(!this.isExpanded()), null)); .event(() => this.setExpanded(!this.isExpanded()), null));
this._register(onHeaderKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow) this._register(onHeaderKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow)
.event(() => this.setExpanded(false), null)); .event(() => this.setExpanded(false), null));
this._register(onHeaderKeyDown.filter(e => e.keyCode === KeyCode.RightArrow) this._register(onHeaderKeyDown.filter(e => e.keyCode === KeyCode.RightArrow)
.event(() => this.setExpanded(true), null)); .event(() => this.setExpanded(true), null));
this._register(domEvent(this.header, 'click') this._register(domEvent(this.header, 'click')
(() => this.setExpanded(!this.isExpanded()), null)); (() => this.setExpanded(!this.isExpanded()), null));
}
this.body = append(this.element, $('.pane-body')); this.body = append(this.element, $('.pane-body'));
this.renderBody(this.body); this.renderBody(this.body);
@@ -402,13 +411,14 @@ export class PaneView extends Disposable {
private el: HTMLElement; private el: HTMLElement;
private paneItems: IPaneItem[] = []; private paneItems: IPaneItem[] = [];
private orthogonalSize: number = 0; private orthogonalSize: number = 0;
private size: number = 0;
private splitview: SplitView; private splitview: SplitView;
private orientation: Orientation;
private animationTimer: number | undefined = undefined; private animationTimer: number | undefined = undefined;
private _onDidDrop = this._register(new Emitter<{ from: Pane, to: Pane }>()); private _onDidDrop = this._register(new Emitter<{ from: Pane, to: Pane }>());
readonly onDidDrop: Event<{ from: Pane, to: Pane }> = this._onDidDrop.event; readonly onDidDrop: Event<{ from: Pane, to: Pane }> = this._onDidDrop.event;
orientation: Orientation;
readonly onDidSashChange: Event<number>; readonly onDidSashChange: Event<number>;
constructor(container: HTMLElement, options: IPaneViewOptions = {}) { constructor(container: HTMLElement, options: IPaneViewOptions = {}) {
@@ -427,6 +437,7 @@ export class PaneView extends Disposable {
const paneItem = { pane: pane, disposable: disposables }; const paneItem = { pane: pane, disposable: disposables };
this.paneItems.splice(index, 0, paneItem); this.paneItems.splice(index, 0, paneItem);
pane.orientation = this.orientation;
pane.orthogonalSize = this.orthogonalSize; pane.orthogonalSize = this.orthogonalSize;
this.splitview.addView(pane, size, index); this.splitview.addView(pane, size, index);
@@ -485,12 +496,39 @@ export class PaneView extends Disposable {
layout(height: number, width: number): void { layout(height: number, width: number): void {
this.orthogonalSize = this.orientation === Orientation.VERTICAL ? width : height; this.orthogonalSize = this.orientation === Orientation.VERTICAL ? width : height;
this.size = this.orientation === Orientation.HORIZONTAL ? width : height;
for (const paneItem of this.paneItems) { for (const paneItem of this.paneItems) {
paneItem.pane.orthogonalSize = this.orthogonalSize; paneItem.pane.orthogonalSize = this.orthogonalSize;
} }
this.splitview.layout(this.orientation === Orientation.HORIZONTAL ? width : height); this.splitview.layout(this.size);
}
flipOrientation(height: number, width: number): void {
this.orientation = this.orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL;
const paneSizes = this.paneItems.map(pane => this.getPaneSize(pane.pane));
this.splitview.dispose();
clearNode(this.el);
this.splitview = this._register(new SplitView(this.el, { orientation: this.orientation }));
const newOrthogonalSize = this.orientation === Orientation.VERTICAL ? width : height;
const newSize = this.orientation === Orientation.HORIZONTAL ? width : height;
this.paneItems.forEach((pane, index) => {
pane.pane.orthogonalSize = newOrthogonalSize;
pane.pane.orientation = this.orientation;
const viewSize = this.size === 0 ? 0 : (newSize * paneSizes[index]) / this.size;
this.splitview.addView(pane.pane, viewSize, index);
});
this.size = newSize;
this.orthogonalSize = newOrthogonalSize;
this.splitview.layout(this.size);
} }
private setupAnimation(): void { private setupAnimation(): void {

View File

@@ -115,6 +115,19 @@ export class VSBuffer {
} }
} }
export function readUInt16LE(source: Uint8Array, offset: number): number {
return (
source[offset]
+ source[offset + 1] * 2 ** 8
);
}
export function writeUInt16LE(destination: Uint8Array, value: number, offset: number): void {
destination[offset] = value;
value = value >>> 8;
destination[offset + 1] = value;
}
export function readUInt32BE(source: Uint8Array, offset: number): number { export function readUInt32BE(source: Uint8Array, offset: number): number {
return ( return (
source[offset] * 2 ** 24 source[offset] * 2 ** 24
@@ -134,11 +147,11 @@ export function writeUInt32BE(destination: Uint8Array, value: number, offset: nu
destination[offset] = value; destination[offset] = value;
} }
function readUInt8(source: Uint8Array, offset: number): number { export function readUInt8(source: Uint8Array, offset: number): number {
return source[offset]; return source[offset];
} }
function writeUInt8(destination: Uint8Array, value: number, offset: number): void { export function writeUInt8(destination: Uint8Array, value: number, offset: number): void {
destination[offset] = value; destination[offset] = value;
} }

View File

@@ -321,6 +321,8 @@ export function prepareQuery(original: string): IPreparedQuery {
let value = stripWildcards(original).replace(/\s/g, ''); // get rid of all wildcards and whitespace let value = stripWildcards(original).replace(/\s/g, ''); // get rid of all wildcards and whitespace
if (isWindows) { if (isWindows) {
value = value.replace(/\//g, sep); // Help Windows users to search for paths when using slash value = value.replace(/\//g, sep); // Help Windows users to search for paths when using slash
} else {
value = value.replace(/\\/g, sep); // Help macOS/Linux users to search for paths when using backslash
} }
const lowercase = value.toLowerCase(); const lowercase = value.toLowerCase();
@@ -451,7 +453,7 @@ function doScoreItem(label: string, description: string | undefined, path: strin
return NO_ITEM_SCORE; return NO_ITEM_SCORE;
} }
export function compareItemsByScore<T>(itemA: T, itemB: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor<T>, cache: ScorerCache, fallbackComparer = fallbackCompare): number { export function compareItemsByScore<T>(itemA: T, itemB: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor<T>, cache: ScorerCache): number {
const itemScoreA = scoreItem(itemA, query, fuzzy, accessor, cache); const itemScoreA = scoreItem(itemA, query, fuzzy, accessor, cache);
const itemScoreB = scoreItem(itemB, query, fuzzy, accessor, cache); const itemScoreB = scoreItem(itemB, query, fuzzy, accessor, cache);
@@ -517,7 +519,16 @@ export function compareItemsByScore<T>(itemA: T, itemB: T, query: IPreparedQuery
return scoreA > scoreB ? -1 : 1; return scoreA > scoreB ? -1 : 1;
} }
// 6.) scores are identical, prefer more compact matches (label and description) // 6.) prefer matches in label over non-label matches
const itemAHasLabelMatches = Array.isArray(itemScoreA.labelMatch) && itemScoreA.labelMatch.length > 0;
const itemBHasLabelMatches = Array.isArray(itemScoreB.labelMatch) && itemScoreB.labelMatch.length > 0;
if (itemAHasLabelMatches && !itemBHasLabelMatches) {
return -1;
} else if (itemBHasLabelMatches && !itemAHasLabelMatches) {
return 1;
}
// 7.) scores are identical, prefer more compact matches (label and description)
const itemAMatchDistance = computeLabelAndDescriptionMatchDistance(itemA, itemScoreA, accessor); const itemAMatchDistance = computeLabelAndDescriptionMatchDistance(itemA, itemScoreA, accessor);
const itemBMatchDistance = computeLabelAndDescriptionMatchDistance(itemB, itemScoreB, accessor); const itemBMatchDistance = computeLabelAndDescriptionMatchDistance(itemB, itemScoreB, accessor);
if (itemAMatchDistance && itemBMatchDistance && itemAMatchDistance !== itemBMatchDistance) { if (itemAMatchDistance && itemBMatchDistance && itemAMatchDistance !== itemBMatchDistance) {
@@ -526,7 +537,7 @@ export function compareItemsByScore<T>(itemA: T, itemB: T, query: IPreparedQuery
// 7.) at this point, scores are identical and match compactness as well // 7.) at this point, scores are identical and match compactness as well
// for both items so we start to use the fallback compare // for both items so we start to use the fallback compare
return fallbackComparer(itemA, itemB, query, accessor); return fallbackCompare(itemA, itemB, query, accessor);
} }
function computeLabelAndDescriptionMatchDistance<T>(item: T, score: IItemScore, accessor: IItemAccessor<T>): number { function computeLabelAndDescriptionMatchDistance<T>(item: T, score: IItemScore, accessor: IItemAccessor<T>): number {

View File

@@ -53,6 +53,12 @@ export namespace Schemas {
export const vscodeRemoteResource = 'vscode-remote-resource'; export const vscodeRemoteResource = 'vscode-remote-resource';
export const userData = 'vscode-userdata'; export const userData = 'vscode-userdata';
export const vscodeCustomEditor = 'vscode-custom-editor';
export const vscodeSettings = 'vscode-settings';
export const webviewPanel = 'webview-panel';
} }
class RemoteAuthoritiesImpl { class RemoteAuthoritiesImpl {

View File

@@ -209,3 +209,17 @@ export const enum OperatingSystem {
Linux = 3 Linux = 3
} }
export const OS = (_isMacintosh ? OperatingSystem.Macintosh : (_isWindows ? OperatingSystem.Windows : OperatingSystem.Linux)); export const OS = (_isMacintosh ? OperatingSystem.Macintosh : (_isWindows ? OperatingSystem.Windows : OperatingSystem.Linux));
let _isLittleEndian = true;
let _isLittleEndianComputed = false;
export function isLittleEndian(): boolean {
if (!_isLittleEndianComputed) {
_isLittleEndianComputed = true;
const test = new Uint8Array(2);
test[0] = 1;
test[1] = 2;
const view = new Uint16Array(test.buffer);
_isLittleEndian = (view[0] === (2 << 8) + 1);
}
return _isLittleEndian;
}

View File

@@ -15,8 +15,16 @@ import { TernarySearchTree } from 'vs/base/common/map';
export const originalFSPath = uriOriginalFSPath; export const originalFSPath = uriOriginalFSPath;
export function getComparisonKey(resource: URI): string { /**
return hasToIgnoreCase(resource) ? resource.toString().toLowerCase() : resource.toString(); * Creates a key from a resource URI to be used to resource comparison and for resource maps.
* URI queries are included, fragments are ignored.
*/
export function getComparisonKey(resource: URI, caseInsensitivePath = hasToIgnoreCase(resource)): string {
let path = resource.path || '/';
if (caseInsensitivePath) {
path = path.toLowerCase();
}
return `${resource.scheme}://${resource.authority.toLowerCase()}/${path}?${resource.query}`;
} }
export function hasToIgnoreCase(resource: URI | undefined): boolean { export function hasToIgnoreCase(resource: URI | undefined): boolean {
@@ -31,29 +39,33 @@ export function basenameOrAuthority(resource: URI): string {
/** /**
* Tests whether a `candidate` URI is a parent or equal of a given `base` URI. * Tests whether a `candidate` URI is a parent or equal of a given `base` URI.
* URI queries must match, fragments are ignored.
* @param base A uri which is "longer" * @param base A uri which is "longer"
* @param parentCandidate A uri which is "shorter" then `base` * @param parentCandidate A uri which is "shorter" then `base`
*/ */
export function isEqualOrParent(base: URI, parentCandidate: URI, ignoreCase = hasToIgnoreCase(base)): boolean { export function isEqualOrParent(base: URI, parentCandidate: URI, ignoreCase = hasToIgnoreCase(base)): boolean {
if (base.scheme === parentCandidate.scheme) { if (base.scheme === parentCandidate.scheme) {
if (base.scheme === Schemas.file) { if (base.scheme === Schemas.file) {
return extpath.isEqualOrParent(originalFSPath(base), originalFSPath(parentCandidate), ignoreCase); return extpath.isEqualOrParent(originalFSPath(base), originalFSPath(parentCandidate), ignoreCase) && base.query === parentCandidate.query;
} }
if (isEqualAuthority(base.authority, parentCandidate.authority)) { if (isEqualAuthority(base.authority, parentCandidate.authority)) {
return extpath.isEqualOrParent(base.path, parentCandidate.path, ignoreCase, '/'); return extpath.isEqualOrParent(base.path || '/', parentCandidate.path || '/', ignoreCase, '/') && base.query === parentCandidate.query;
} }
} }
return false; return false;
} }
/** /**
* Tests wheter the two authorities are the same * Tests whether the two authorities are the same
*/ */
export function isEqualAuthority(a1: string, a2: string) { export function isEqualAuthority(a1: string, a2: string) {
return a1 === a2 || equalsIgnoreCase(a1, a2); return a1 === a2 || equalsIgnoreCase(a1, a2);
} }
export function isEqual(first: URI | undefined, second: URI | undefined, ignoreCase = hasToIgnoreCase(first)): boolean { /**
* Tests whether two resources are the same. URI queries must match, fragments are ignored unless requested.
*/
export function isEqual(first: URI | undefined, second: URI | undefined, caseInsensitivePath = hasToIgnoreCase(first), ignoreFragment = true): boolean {
if (first === second) { if (first === second) {
return true; return true;
} }
@@ -67,7 +79,7 @@ export function isEqual(first: URI | undefined, second: URI | undefined, ignoreC
} }
const p1 = first.path || '/', p2 = second.path || '/'; const p1 = first.path || '/', p2 = second.path || '/';
return p1 === p2 || ignoreCase && equalsIgnoreCase(p1 || '/', p2 || '/'); return (p1 === p2 || caseInsensitivePath && equalsIgnoreCase(p1, p2)) && first.query === second.query && (ignoreFragment || first.fragment === second.fragment);
} }
export function basename(resource: URI): string { export function basename(resource: URI): string {
@@ -88,13 +100,15 @@ export function dirname(resource: URI): URI {
if (resource.path.length === 0) { if (resource.path.length === 0) {
return resource; return resource;
} }
let dirname;
if (resource.scheme === Schemas.file) { if (resource.scheme === Schemas.file) {
return URI.file(paths.dirname(originalFSPath(resource))); dirname = URI.file(paths.dirname(originalFSPath(resource))).path;
} } else {
let dirname = paths.posix.dirname(resource.path); dirname = paths.posix.dirname(resource.path);
if (resource.authority && dirname.length && dirname.charCodeAt(0) !== CharCode.Slash) { if (resource.authority && dirname.length && dirname.charCodeAt(0) !== CharCode.Slash) {
console.error(`dirname("${resource.toString})) resulted in a relative path`); console.error(`dirname("${resource.toString})) resulted in a relative path`);
dirname = '/'; // If a URI contains an authority component, then the path component must either be empty or begin with a CharCode.Slash ("/") character dirname = '/'; // If a URI contains an authority component, then the path component must either be empty or begin with a CharCode.Slash ("/") character
}
} }
return resource.with({ return resource.with({
path: dirname path: dirname
@@ -189,7 +203,7 @@ export function addTrailingPathSeparator(resource: URI, sep: string = paths.sep)
* Returns a relative path between two URIs. If the URIs don't have the same schema or authority, `undefined` is returned. * Returns a relative path between two URIs. If the URIs don't have the same schema or authority, `undefined` is returned.
* The returned relative path always uses forward slashes. * The returned relative path always uses forward slashes.
*/ */
export function relativePath(from: URI, to: URI, ignoreCase = hasToIgnoreCase(from)): string | undefined { export function relativePath(from: URI, to: URI, caseInsensitivePath = hasToIgnoreCase(from)): string | undefined {
if (from.scheme !== to.scheme || !isEqualAuthority(from.authority, to.authority)) { if (from.scheme !== to.scheme || !isEqualAuthority(from.authority, to.authority)) {
return undefined; return undefined;
} }
@@ -198,7 +212,7 @@ export function relativePath(from: URI, to: URI, ignoreCase = hasToIgnoreCase(fr
return isWindows ? extpath.toSlashes(relativePath) : relativePath; return isWindows ? extpath.toSlashes(relativePath) : relativePath;
} }
let fromPath = from.path || '/', toPath = to.path || '/'; let fromPath = from.path || '/', toPath = to.path || '/';
if (ignoreCase) { if (caseInsensitivePath) {
// make casing of fromPath match toPath // make casing of fromPath match toPath
let i = 0; let i = 0;
for (const len = Math.min(fromPath.length, toPath.length); i < len; i++) { for (const len = Math.min(fromPath.length, toPath.length); i < len; i++) {

View File

@@ -55,8 +55,8 @@
margin-bottom: -2px; margin-bottom: -2px;
} }
.quick-input-widget.quick-navigate-mode .quick-input-header { .quick-input-widget.hidden-input .quick-input-header {
/* reduce margins and paddings in quick navigate mode */ /* reduce margins and paddings when input box hidden */
padding: 0; padding: 0;
margin-bottom: 0; margin-bottom: 0;
} }
@@ -132,8 +132,8 @@
margin-top: 6px; margin-top: 6px;
} }
.quick-input-widget.quick-navigate-mode .quick-input-list { .quick-input-widget.hidden-input .quick-input-list {
margin-top: 0; /* reduce margins in quick navigate mode */ margin-top: 0; /* reduce margins when input box hidden */
} }
.quick-input-list .monaco-list { .quick-input-list .monaco-list {

View File

@@ -7,7 +7,7 @@ import 'vs/css!./media/quickInput';
import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent, NO_KEY_MODS } from 'vs/base/parts/quickinput/common/quickInput'; import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent, NO_KEY_MODS } from 'vs/base/parts/quickinput/common/quickInput';
import * as dom from 'vs/base/browser/dom'; import * as dom from 'vs/base/browser/dom';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { QuickInputList } from './quickInputList'; import { QuickInputList, QuickInputListFocus } from './quickInputList';
import { QuickInputBox } from './quickInputBox'; import { QuickInputBox } from './quickInputBox';
import { KeyCode } from 'vs/base/common/keyCodes'; import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
@@ -364,7 +364,7 @@ class QuickInput extends Disposable implements IQuickInput {
readonly onDispose = this.onDisposeEmitter.event; readonly onDispose = this.onDisposeEmitter.event;
public dispose(): void { dispose(): void {
this.hide(); this.hide();
this.onDisposeEmitter.fire(); this.onDisposeEmitter.fire();
@@ -391,6 +391,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
private _matchOnLabel = true; private _matchOnLabel = true;
private _sortByLabel = true; private _sortByLabel = true;
private _autoFocusOnList = true; private _autoFocusOnList = true;
private _autoFocusSecondEntry = false;
private _activeItems: T[] = []; private _activeItems: T[] = [];
private activeItemsUpdated = false; private activeItemsUpdated = false;
private activeItemsToConfirm: T[] | null = []; private activeItemsToConfirm: T[] | null = [];
@@ -408,6 +409,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
private _customButtonLabel: string | undefined; private _customButtonLabel: string | undefined;
private _customButtonHover: string | undefined; private _customButtonHover: string | undefined;
private _quickNavigate: IQuickNavigateConfiguration | undefined; private _quickNavigate: IQuickNavigateConfiguration | undefined;
private _hideInput: boolean | undefined;
get quickNavigate() { get quickNavigate() {
return this._quickNavigate; return this._quickNavigate;
@@ -460,10 +462,6 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
set items(items: Array<T | IQuickPickSeparator>) { set items(items: Array<T | IQuickPickSeparator>) {
this._items = items; this._items = items;
this.itemsUpdated = true; this.itemsUpdated = true;
if (this._items.length === 0) {
// quick-navigate requires at least 1 item
this._quickNavigate = undefined;
}
this.update(); this.update();
} }
@@ -520,7 +518,6 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.update(); this.update();
} }
get autoFocusOnList() { get autoFocusOnList() {
return this._autoFocusOnList; return this._autoFocusOnList;
} }
@@ -530,6 +527,14 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.update(); this.update();
} }
get autoFocusSecondEntry() {
return this._autoFocusSecondEntry;
}
set autoFocusSecondEntry(autoFocusSecondEntry: boolean) {
this._autoFocusSecondEntry = autoFocusSecondEntry;
}
get activeItems() { get activeItems() {
return this._activeItems; return this._activeItems;
} }
@@ -614,14 +619,23 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.update(); this.update();
} }
public inputHasFocus(): boolean { inputHasFocus(): boolean {
return this.visible ? this.ui.inputBox.hasFocus() : false; return this.visible ? this.ui.inputBox.hasFocus() : false;
} }
public focusOnInput() { focusOnInput() {
this.ui.inputBox.setFocus(); this.ui.inputBox.setFocus();
} }
get hideInput() {
return !!this._hideInput;
}
set hideInput(hideInput: boolean) {
this._hideInput = hideInput;
this.update();
}
onDidChangeSelection = this.onDidChangeSelectionEmitter.event; onDidChangeSelection = this.onDidChangeSelectionEmitter.event;
onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event; onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event;
@@ -629,7 +643,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
private trySelectFirst() { private trySelectFirst() {
if (this.autoFocusOnList) { if (this.autoFocusOnList) {
if (!this.ui.isScreenReaderOptimized() && !this.canSelectMany) { if (!this.ui.isScreenReaderOptimized() && !this.canSelectMany) {
this.ui.list.focus('First'); this.ui.list.focus(QuickInputListFocus.First);
} }
} }
} }
@@ -656,7 +670,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.visibleDisposables.add(this.ui.inputBox.onKeyDown(event => { this.visibleDisposables.add(this.ui.inputBox.onKeyDown(event => {
switch (event.keyCode) { switch (event.keyCode) {
case KeyCode.DownArrow: case KeyCode.DownArrow:
this.ui.list.focus('Next'); this.ui.list.focus(QuickInputListFocus.Next);
if (this.canSelectMany) { if (this.canSelectMany) {
this.ui.list.domFocus(); this.ui.list.domFocus();
} }
@@ -664,9 +678,9 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
break; break;
case KeyCode.UpArrow: case KeyCode.UpArrow:
if (this.ui.list.getFocusedElements().length) { if (this.ui.list.getFocusedElements().length) {
this.ui.list.focus('Previous'); this.ui.list.focus(QuickInputListFocus.Previous);
} else { } else {
this.ui.list.focus('Last'); this.ui.list.focus(QuickInputListFocus.Last);
} }
if (this.canSelectMany) { if (this.canSelectMany) {
this.ui.list.domFocus(); this.ui.list.domFocus();
@@ -675,9 +689,9 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
break; break;
case KeyCode.PageDown: case KeyCode.PageDown:
if (this.ui.list.getFocusedElements().length) { if (this.ui.list.getFocusedElements().length) {
this.ui.list.focus('NextPage'); this.ui.list.focus(QuickInputListFocus.NextPage);
} else { } else {
this.ui.list.focus('First'); this.ui.list.focus(QuickInputListFocus.First);
} }
if (this.canSelectMany) { if (this.canSelectMany) {
this.ui.list.domFocus(); this.ui.list.domFocus();
@@ -686,9 +700,9 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
break; break;
case KeyCode.PageUp: case KeyCode.PageUp:
if (this.ui.list.getFocusedElements().length) { if (this.ui.list.getFocusedElements().length) {
this.ui.list.focus('PreviousPage'); this.ui.list.focus(QuickInputListFocus.PreviousPage);
} else { } else {
this.ui.list.focus('Last'); this.ui.list.focus(QuickInputListFocus.Last);
} }
if (this.canSelectMany) { if (this.canSelectMany) {
this.ui.list.domFocus(); this.ui.list.domFocus();
@@ -721,7 +735,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.onDidAcceptEmitter.fire({ inBackground: false }); this.onDidAcceptEmitter.fire({ inBackground: false });
})); }));
this.visibleDisposables.add(this.ui.onDidCustom(() => { this.visibleDisposables.add(this.ui.onDidCustom(() => {
this.onDidCustomEmitter.fire(undefined); this.onDidCustomEmitter.fire();
})); }));
this.visibleDisposables.add(this.ui.list.onDidChangeFocus(focusedItems => { this.visibleDisposables.add(this.ui.list.onDidChangeFocus(focusedItems => {
if (this.activeItemsUpdated) { if (this.activeItemsUpdated) {
@@ -768,7 +782,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
private registerQuickNavigation() { private registerQuickNavigation() {
return dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, e => { return dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, e => {
if (this.canSelectMany || !this.quickNavigate) { if (this.canSelectMany || !this._quickNavigate) {
return; return;
} }
@@ -776,7 +790,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
const keyCode = keyboardEvent.keyCode; const keyCode = keyboardEvent.keyCode;
// Select element when keys are pressed that signal it // Select element when keys are pressed that signal it
const quickNavKeys = this.quickNavigate.keybindings; const quickNavKeys = this._quickNavigate.keybindings;
const wasTriggerKeyPressed = quickNavKeys.some(k => { const wasTriggerKeyPressed = quickNavKeys.some(k => {
const [firstPart, chordPart] = k.getParts(); const [firstPart, chordPart] = k.getParts();
if (chordPart) { if (chordPart) {
@@ -806,10 +820,16 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
return false; return false;
}); });
if (wasTriggerKeyPressed && this.activeItems[0]) { if (wasTriggerKeyPressed) {
this._selectedItems = [this.activeItems[0]]; if (this.activeItems[0]) {
this.onDidChangeSelectionEmitter.fire(this.selectedItems); this._selectedItems = [this.activeItems[0]];
this.onDidAcceptEmitter.fire({ inBackground: false }); this.onDidChangeSelectionEmitter.fire(this.selectedItems);
this.onDidAcceptEmitter.fire({ inBackground: false });
}
// Unset quick navigate after press. It is only valid once
// and should not result in any behaviour change afterwards
// if the picker remains open because there was no active item
this._quickNavigate = undefined;
} }
}); });
} }
@@ -818,11 +838,21 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
if (!this.visible) { if (!this.visible) {
return; return;
} }
dom.toggleClass(this.ui.container, 'quick-navigate-mode', !!this._quickNavigate); const hideInput = !!this._hideInput && this._items.length > 0; // do not allow to hide input without items
const ok = this.ok === 'default' ? this.canSelectMany : this.ok; dom.toggleClass(this.ui.container, 'hidden-input', hideInput);
const visibilities: Visibilities = this.canSelectMany ? const visibilities: Visibilities = {
{ title: !!this.title || !!this.step, description: !!this.description, checkAll: true, inputBox: !this._quickNavigate, progressBar: !this._quickNavigate, visibleCount: true, count: true, ok, list: true, message: !!this.validationMessage, customButton: this.customButton } : title: !!this.title || !!this.step,
{ title: !!this.title || !!this.step, description: !!this.description, inputBox: !this._quickNavigate, progressBar: !this._quickNavigate, visibleCount: true, list: true, message: !!this.validationMessage, customButton: this.customButton, ok }; description: !!this.description,
checkAll: this.canSelectMany,
inputBox: !hideInput,
progressBar: !hideInput,
visibleCount: true,
count: this.canSelectMany,
ok: this.ok === 'default' ? this.canSelectMany : this.ok,
list: true,
message: !!this.validationMessage,
customButton: this.customButton
};
this.ui.setVisibilities(visibilities); this.ui.setVisibilities(visibilities);
super.update(); super.update();
if (this.ui.inputBox.value !== this.value) { if (this.ui.inputBox.value !== this.value) {
@@ -844,17 +874,16 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.ui.list.sortByLabel = this.sortByLabel; this.ui.list.sortByLabel = this.sortByLabel;
if (this.itemsUpdated) { if (this.itemsUpdated) {
this.itemsUpdated = false; this.itemsUpdated = false;
const previousItemCount = this.ui.list.getElementsCount();
this.ui.list.setElements(this.items); this.ui.list.setElements(this.items);
this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); this.ui.list.filter(this.filterValue(this.ui.inputBox.value));
this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked();
this.ui.visibleCount.setCount(this.ui.list.getVisibleCount()); this.ui.visibleCount.setCount(this.ui.list.getVisibleCount());
this.ui.count.setCount(this.ui.list.getCheckedCount()); this.ui.count.setCount(this.ui.list.getCheckedCount());
this.trySelectFirst(); if (this._autoFocusSecondEntry) {
if (this._quickNavigate && previousItemCount === 0 && this.items.length > 1) { this.ui.list.focus(QuickInputListFocus.Second);
// quick navigate: automatically focus the second entry this._autoFocusSecondEntry = false; // only valid once, then unset
// so that upon release the item is picked directly } else {
this.ui.list.focus('Next'); this.trySelectFirst();
} }
} }
if (this.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) { if (this.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) {
@@ -985,7 +1014,7 @@ class InputBox extends QuickInput implements IInputBox {
this._value = value; this._value = value;
this.onDidValueChangeEmitter.fire(value); this.onDidValueChangeEmitter.fire(value);
})); }));
this.visibleDisposables.add(this.ui.onDidAccept(() => this.onDidAcceptEmitter.fire(undefined))); this.visibleDisposables.add(this.ui.onDidAccept(() => this.onDidAcceptEmitter.fire()));
this.valueSelectionUpdated = true; this.valueSelectionUpdated = true;
} }
super.show(); super.show();
@@ -1039,10 +1068,12 @@ export class QuickInputController extends Disposable {
private parentElement: HTMLElement; private parentElement: HTMLElement;
private styles: IQuickInputStyles; private styles: IQuickInputStyles;
private onShowEmitter = new Emitter<void>(); private onShowEmitter = new Emitter<void>();
readonly onShow = this.onShowEmitter.event;
private onHideEmitter = new Emitter<void>(); private onHideEmitter = new Emitter<void>();
public onShow = this.onShowEmitter.event; readonly onHide = this.onHideEmitter.event;
public onHide = this.onHideEmitter.event;
constructor(private options: IQuickInputOptions) { constructor(private options: IQuickInputOptions) {
super(); super();
@@ -1517,7 +1548,7 @@ export class QuickInputController extends Disposable {
} }
} }
public hide(focusLost?: boolean) { hide(focusLost?: boolean) {
const controller = this.controller; const controller = this.controller;
if (controller) { if (controller) {
this.controller = null; this.controller = null;
@@ -1544,14 +1575,21 @@ export class QuickInputController extends Disposable {
navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration) { navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration) {
if (this.isDisplayed() && this.getUI().list.isDisplayed()) { if (this.isDisplayed() && this.getUI().list.isDisplayed()) {
this.getUI().list.focus(next ? 'Next' : 'Previous'); this.getUI().list.focus(next ? QuickInputListFocus.Next : QuickInputListFocus.Previous);
if (quickNavigate && this.controller instanceof QuickPick) { if (quickNavigate && this.controller instanceof QuickPick) {
this.controller.quickNavigate = quickNavigate; this.controller.quickNavigate = quickNavigate;
} }
} }
} }
async accept() { async accept(keyMods: IKeyMods = { alt: false, ctrlCmd: false }) {
// When accepting the item programmatically, it is important that
// we update `keyMods` either from the provided set or unset it
// because the accept did not happen from mouse or keyboard
// interaction on the list itself
this.keyMods.alt = keyMods.alt;
this.keyMods.ctrlCmd = keyMods.ctrlCmd;
this.onDidAcceptEmitter.fire(); this.onDidAcceptEmitter.fire();
} }
@@ -1583,7 +1621,7 @@ export class QuickInputController extends Disposable {
} }
} }
public applyStyles(styles: IQuickInputStyles) { applyStyles(styles: IQuickInputStyles) {
this.styles = styles; this.styles = styles;
this.updateStyles(); this.updateStyles();
} }

View File

@@ -222,6 +222,16 @@ class ListElementDelegate implements IListVirtualDelegate<ListElement> {
} }
} }
export enum QuickInputListFocus {
First = 1,
Second,
Last,
Next,
Previous,
NextPage,
PreviousPage
}
export class QuickInputList { export class QuickInputList {
readonly id: string; readonly id: string;
@@ -307,6 +317,18 @@ export class QuickInputList {
this._onLeave.fire(); this._onLeave.fire();
} }
})); }));
this.disposables.push(this.list.onContextMenu(e => {
if (typeof e.index === 'number') {
e.browserEvent.preventDefault();
// we want to treat a context menu event as
// a gesture to open the item at the index
// since we do not have any context menu
// this enables for example macOS to Ctrl-
// click on an item to open it.
this.list.setSelection([e.index]);
}
}));
} }
@memoize @memoize
@@ -430,7 +452,10 @@ export class QuickInputList {
.filter(item => this.elementsToIndexes.has(item)) .filter(item => this.elementsToIndexes.has(item))
.map(item => this.elementsToIndexes.get(item)!)); .map(item => this.elementsToIndexes.get(item)!));
if (items.length > 0) { if (items.length > 0) {
this.list.reveal(this.list.getFocus()[0]); const focused = this.list.getFocus()[0];
if (typeof focused === 'number') {
this.list.reveal(focused);
}
} }
} }
@@ -474,19 +499,51 @@ export class QuickInputList {
this.list.getHTMLElement().style.pointerEvents = value ? null : 'none'; this.list.getHTMLElement().style.pointerEvents = value ? null : 'none';
} }
focus(what: 'First' | 'Last' | 'Next' | 'Previous' | 'NextPage' | 'PreviousPage'): void { focus(what: QuickInputListFocus): void {
if (!this.list.length) { if (!this.list.length) {
return; return;
} }
if ((what === 'Next' || what === 'NextPage') && this.list.getFocus()[0] === this.list.length - 1) { if ((what === QuickInputListFocus.Next || what === QuickInputListFocus.NextPage) && this.list.getFocus()[0] === this.list.length - 1) {
what = 'First'; what = QuickInputListFocus.First;
} }
if ((what === 'Previous' || what === 'PreviousPage') && this.list.getFocus()[0] === 0) {
what = 'Last'; if ((what === QuickInputListFocus.Previous || what === QuickInputListFocus.PreviousPage) && this.list.getFocus()[0] === 0) {
what = QuickInputListFocus.Last;
}
if (what === QuickInputListFocus.Second && this.list.length < 2) {
what = QuickInputListFocus.First;
}
switch (what) {
case QuickInputListFocus.First:
this.list.focusFirst();
break;
case QuickInputListFocus.Second:
this.list.focusNth(1);
break;
case QuickInputListFocus.Last:
this.list.focusLast();
break;
case QuickInputListFocus.Next:
this.list.focusNext();
break;
case QuickInputListFocus.Previous:
this.list.focusPrevious();
break;
case QuickInputListFocus.NextPage:
this.list.focusNextPage();
break;
case QuickInputListFocus.PreviousPage:
this.list.focusPreviousPage();
break;
}
const focused = this.list.getFocus()[0];
if (typeof focused === 'number') {
this.list.reveal(focused);
} }
this.list['focus' + what as 'focusFirst' | 'focusLast' | 'focusNext' | 'focusPrevious' | 'focusNextPage' | 'focusPreviousPage']();
this.list.reveal(this.list.getFocus()[0]);
} }
clearFocus() { clearFocus() {

View File

@@ -237,6 +237,14 @@ export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
autoFocusOnList: boolean; autoFocusOnList: boolean;
/**
* If enabled, will try to select the second entry of the picks
* once they appear instead of the first one. This is useful
* e.g. when `quickNavigate` is enabled to be able to select
* a previous entry by just releasing the quick nav keys.
*/
autoFocusSecondEntry: boolean;
quickNavigate: IQuickNavigateConfiguration | undefined; quickNavigate: IQuickNavigateConfiguration | undefined;
activeItems: ReadonlyArray<T>; activeItems: ReadonlyArray<T>;
@@ -256,6 +264,13 @@ export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
inputHasFocus(): boolean; inputHasFocus(): boolean;
focusOnInput(): void; focusOnInput(): void;
/**
* Hides the input box from the picker UI. This is typically used
* in combination with quick-navigation where no search UI should
* be presented.
*/
hideInput: boolean;
} }
export interface IInputBox extends IQuickInput { export interface IInputBox extends IQuickInput {

View File

@@ -8,6 +8,7 @@ import * as scorer from 'vs/base/common/fuzzyScorer';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { basename, dirname, sep } from 'vs/base/common/path'; import { basename, dirname, sep } from 'vs/base/common/path';
import { isWindows } from 'vs/base/common/platform'; import { isWindows } from 'vs/base/common/platform';
import { Schemas } from 'vs/base/common/network';
class ResourceAccessorClass implements scorer.IItemAccessor<URI> { class ResourceAccessorClass implements scorer.IItemAccessor<URI> {
@@ -49,8 +50,8 @@ function scoreItem<T>(item: T, query: string, fuzzy: boolean, accessor: scorer.I
return scorer.scoreItem(item, scorer.prepareQuery(query), fuzzy, accessor, cache); return scorer.scoreItem(item, scorer.prepareQuery(query), fuzzy, accessor, cache);
} }
function compareItemsByScore<T>(itemA: T, itemB: T, query: string, fuzzy: boolean, accessor: scorer.IItemAccessor<T>, cache: scorer.ScorerCache, fallbackComparer?: (itemA: T, itemB: T, query: scorer.IPreparedQuery, accessor: scorer.IItemAccessor<T>) => number): number { function compareItemsByScore<T>(itemA: T, itemB: T, query: string, fuzzy: boolean, accessor: scorer.IItemAccessor<T>, cache: scorer.ScorerCache): number {
return scorer.compareItemsByScore(itemA, itemB, scorer.prepareQuery(query), fuzzy, accessor, cache, fallbackComparer as any); return scorer.compareItemsByScore(itemA, itemB, scorer.prepareQuery(query), fuzzy, accessor, cache);
} }
const NullAccessor = new NullAccessorClass(); const NullAccessor = new NullAccessorClass();
@@ -279,6 +280,19 @@ suite('Fuzzy Scorer', () => {
assert.ok(!res.score); assert.ok(!res.score);
}); });
test('scoreItem - match if using slash or backslash (local, remote resource)', function () {
const localResource = URI.file('abcde/super/duper');
const remoteResource = URI.from({ scheme: Schemas.vscodeRemote, path: 'abcde/super/duper' });
for (const resource of [localResource, remoteResource]) {
let res = scoreItem(resource, 'abcde\\super\\duper', true, ResourceAccessor, cache);
assert.ok(res.score);
res = scoreItem(resource, 'abcde/super/duper', true, ResourceAccessor, cache);
assert.ok(res.score);
}
});
test('compareItemsByScore - identity', function () { test('compareItemsByScore - identity', function () {
const resourceA = URI.file('/some/path/fileA.txt'); const resourceA = URI.file('/some/path/fileA.txt');
const resourceB = URI.file('/some/path/other/fileB.txt'); const resourceB = URI.file('/some/path/other/fileB.txt');
@@ -509,33 +523,13 @@ suite('Fuzzy Scorer', () => {
assert.equal(res[2], resourceC); assert.equal(res[2], resourceC);
}); });
test('compareFilesByScore - allow to provide fallback sorter (bug #31591)', function () { test('compareFilesByScore - prefer matches in label over description if scores are otherwise equal', function () {
const resourceA = URI.file('virtual/vscode.d.ts'); const resourceA = URI.file('parts/quick/arrow-left-dark.svg');
const resourceB = URI.file('vscode/src/vs/vscode.d.ts'); const resourceB = URI.file('parts/quickopen/quickopen.ts');
let query = 'vscode'; let query = 'partsquick';
let res = [resourceA, resourceB].sort((r1, r2) => { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache));
return compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache, (r1, r2, query, ResourceAccessor) => {
if (r1 as any /* TS fail */ === resourceA) {
return -1;
}
return 1;
});
});
assert.equal(res[0], resourceA);
assert.equal(res[1], resourceB);
res = [resourceB, resourceA].sort((r1, r2) => {
return compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache, (r1, r2, query, ResourceAccessor) => {
if (r1 as any /* TS fail */ === resourceB) {
return -1;
}
return 1;
});
});
assert.equal(res[0], resourceB); assert.equal(res[0], resourceB);
assert.equal(res[1], resourceA); assert.equal(res[1], resourceA);
}); });

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as assert from 'assert'; import * as assert from 'assert';
import { dirname, basename, distinctParents, joinPath, isEqual, isEqualOrParent, hasToIgnoreCase, normalizePath, isAbsolutePath, relativePath, removeTrailingPathSeparator, hasTrailingPathSeparator, resolvePath, addTrailingPathSeparator } from 'vs/base/common/resources'; import { dirname, basename, distinctParents, joinPath, isEqual, isEqualOrParent, hasToIgnoreCase, normalizePath, isAbsolutePath, relativePath, removeTrailingPathSeparator, hasTrailingPathSeparator, resolvePath, addTrailingPathSeparator, getComparisonKey } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { isWindows } from 'vs/base/common/platform'; import { isWindows } from 'vs/base/common/platform';
import { toSlashes } from 'vs/base/common/extpath'; import { toSlashes } from 'vs/base/common/extpath';
@@ -66,6 +66,8 @@ suite('Resources', () => {
// does not explode (https://github.com/Microsoft/vscode/issues/41987) // does not explode (https://github.com/Microsoft/vscode/issues/41987)
dirname(URI.from({ scheme: 'file', authority: '/users/someone/portal.h' })); dirname(URI.from({ scheme: 'file', authority: '/users/someone/portal.h' }));
assert.equal(dirname(URI.parse('foo://a/b/c?q')).toString(), 'foo://a/b?q');
}); });
test('basename', () => { test('basename', () => {
@@ -156,6 +158,7 @@ suite('Resources', () => {
assert.equal(normalizePath(URI.parse('foo://a/foo/foo/./../some/../bar')).toString(), 'foo://a/foo/bar'); assert.equal(normalizePath(URI.parse('foo://a/foo/foo/./../some/../bar')).toString(), 'foo://a/foo/bar');
assert.equal(normalizePath(URI.parse('foo://a')).toString(), 'foo://a'); assert.equal(normalizePath(URI.parse('foo://a')).toString(), 'foo://a');
assert.equal(normalizePath(URI.parse('foo://a/')).toString(), 'foo://a/'); assert.equal(normalizePath(URI.parse('foo://a/')).toString(), 'foo://a/');
assert.equal(normalizePath(URI.parse('foo://a/foo/./bar?q=1')).toString(), URI.parse('foo://a/foo/bar?q%3D1').toString());
}); });
test('isAbsolute', () => { test('isAbsolute', () => {
@@ -233,7 +236,7 @@ suite('Resources', () => {
}); });
function assertEqualURI(actual: URI, expected: URI, message?: string) { function assertEqualURI(actual: URI, expected: URI, message?: string) {
if (!isEqual(expected, actual)) { if (!isEqual(expected, actual, hasToIgnoreCase(expected), false)) {
assert.equal(actual.toString(), expected.toString(), message); assert.equal(actual.toString(), expected.toString(), message);
} }
} }
@@ -259,7 +262,7 @@ suite('Resources', () => {
assertRelativePath(URI.parse('foo://a'), URI.parse('foo://a'), ''); assertRelativePath(URI.parse('foo://a'), URI.parse('foo://a'), '');
assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a/'), ''); assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a/'), '');
assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a'), ''); assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a'), '');
assertRelativePath(URI.parse('foo://a/foo?q'), URI.parse('foo://a/foo/bar#h'), 'bar'); assertRelativePath(URI.parse('foo://a/foo?q'), URI.parse('foo://a/foo/bar#h'), 'bar', true);
assertRelativePath(URI.parse('foo://'), URI.parse('foo://a/b'), undefined); assertRelativePath(URI.parse('foo://'), URI.parse('foo://a/b'), undefined);
assertRelativePath(URI.parse('foo://a2/b'), URI.parse('foo://a/b'), undefined); assertRelativePath(URI.parse('foo://a2/b'), URI.parse('foo://a/b'), undefined);
assertRelativePath(URI.parse('goo://a/b'), URI.parse('foo://a/b'), undefined); assertRelativePath(URI.parse('goo://a/b'), URI.parse('foo://a/b'), undefined);
@@ -343,26 +346,44 @@ suite('Resources', () => {
}); });
function assertIsEqual(u1: URI, u2: URI, ignoreCase: boolean, expected: boolean) {
assert.equal(isEqual(u1, u2, ignoreCase), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`);
assert.equal(getComparisonKey(u1, ignoreCase) === getComparisonKey(u2, ignoreCase), expected, `comparison keys ${u1.toString()}, ${u2.toString()}`);
assert.equal(isEqualOrParent(u1, u2, ignoreCase), expected, `isEqualOrParent ${u1.toString()}, ${u2.toString()}`);
}
test('isEqual', () => { test('isEqual', () => {
let fileURI = isWindows ? URI.file('c:\\foo\\bar') : URI.file('/foo/bar'); let fileURI = isWindows ? URI.file('c:\\foo\\bar') : URI.file('/foo/bar');
let fileURI2 = isWindows ? URI.file('C:\\foo\\Bar') : URI.file('/foo/Bar'); let fileURI2 = isWindows ? URI.file('C:\\foo\\Bar') : URI.file('/foo/Bar');
assert.equal(isEqual(fileURI, fileURI, true), true); assertIsEqual(fileURI, fileURI, true, true);
assert.equal(isEqual(fileURI, fileURI, false), true); assertIsEqual(fileURI, fileURI, false, true);
assert.equal(isEqual(fileURI, fileURI, hasToIgnoreCase(fileURI)), true); assertIsEqual(fileURI, fileURI, hasToIgnoreCase(fileURI), true);
assert.equal(isEqual(fileURI, fileURI2, true), true); assertIsEqual(fileURI, fileURI2, true, true);
assert.equal(isEqual(fileURI, fileURI2, false), false); assertIsEqual(fileURI, fileURI2, false, false);
let fileURI3 = URI.parse('foo://server:453/foo/bar'); let fileURI3 = URI.parse('foo://server:453/foo/bar');
let fileURI4 = URI.parse('foo://server:453/foo/Bar'); let fileURI4 = URI.parse('foo://server:453/foo/Bar');
assert.equal(isEqual(fileURI3, fileURI3, true), true); assertIsEqual(fileURI3, fileURI3, true, true);
assert.equal(isEqual(fileURI3, fileURI3, false), true); assertIsEqual(fileURI3, fileURI3, false, true);
assert.equal(isEqual(fileURI3, fileURI3, hasToIgnoreCase(fileURI3)), true); assertIsEqual(fileURI3, fileURI3, hasToIgnoreCase(fileURI3), true);
assert.equal(isEqual(fileURI3, fileURI4, true), true); assertIsEqual(fileURI3, fileURI4, true, true);
assert.equal(isEqual(fileURI3, fileURI4, false), false); assertIsEqual(fileURI3, fileURI4, false, false);
assert.equal(isEqual(fileURI, fileURI3, true), false); assertIsEqual(fileURI, fileURI3, true, false);
assert.equal(isEqual(URI.parse('foo://server'), URI.parse('foo://server/')), true); assertIsEqual(URI.parse('foo://server'), URI.parse('foo://server/'), true, true);
assertIsEqual(URI.parse('foo://server/foo'), URI.parse('foo://server/foo/'), true, false);
assertIsEqual(URI.parse('foo://server/foo'), URI.parse('foo://server/foo?'), true, true);
let fileURI5 = URI.parse('foo://server:453/foo/bar?q=1');
let fileURI6 = URI.parse('foo://server:453/foo/bar#xy');
assertIsEqual(fileURI5, fileURI5, true, true);
assertIsEqual(fileURI5, fileURI3, true, false);
assertIsEqual(fileURI6, fileURI6, true, true);
assertIsEqual(fileURI6, fileURI5, true, false);
assertIsEqual(fileURI6, fileURI3, true, true);
}); });
test('isEqualOrParent', () => { test('isEqualOrParent', () => {
@@ -388,5 +409,12 @@ suite('Resources', () => {
assert.equal(isEqualOrParent(fileURI3, fileURI4, false), true, '14'); assert.equal(isEqualOrParent(fileURI3, fileURI4, false), true, '14');
assert.equal(isEqualOrParent(fileURI3, fileURI, true), false, '15'); assert.equal(isEqualOrParent(fileURI3, fileURI, true), false, '15');
assert.equal(isEqualOrParent(fileURI5, fileURI5, true), true, '16'); assert.equal(isEqualOrParent(fileURI5, fileURI5, true), true, '16');
let fileURI6 = URI.parse('foo://server:453/foo?q=1');
let fileURI7 = URI.parse('foo://server:453/foo/bar?q=1');
assert.equal(isEqualOrParent(fileURI6, fileURI5, true), false, '17');
assert.equal(isEqualOrParent(fileURI6, fileURI6, true), true, '18');
assert.equal(isEqualOrParent(fileURI7, fileURI6, true), true, '19');
assert.equal(isEqualOrParent(fileURI7, fileURI5, true), false, '20');
}); });
}); });

View File

@@ -52,7 +52,7 @@ import { IProductService } from 'vs/platform/product/common/productService';
import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync';
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, UserDataSyncStoreServiceChannel, UserDataSyncBackupStoreServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc';
import { IElectronService } from 'vs/platform/electron/node/electron'; import { IElectronService } from 'vs/platform/electron/node/electron';
import { LoggerService } from 'vs/platform/log/node/loggerService'; import { LoggerService } from 'vs/platform/log/node/loggerService';
import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog';
@@ -219,14 +219,6 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat
const authTokenChannel = new AuthenticationTokenServiceChannel(authTokenService); const authTokenChannel = new AuthenticationTokenServiceChannel(authTokenService);
server.registerChannel('authToken', authTokenChannel); server.registerChannel('authToken', authTokenChannel);
const userDataSyncStoreService = accessor.get(IUserDataSyncStoreService);
const userDataSyncStoreServiceChannel = new UserDataSyncStoreServiceChannel(userDataSyncStoreService);
server.registerChannel('userDataSyncStoreService', userDataSyncStoreServiceChannel);
const userDataSyncBackupStoreService = accessor.get(IUserDataSyncBackupStoreService);
const userDataSyncBackupStoreServiceChannel = new UserDataSyncBackupStoreServiceChannel(userDataSyncBackupStoreService);
server.registerChannel('userDataSyncBackupStoreService', userDataSyncBackupStoreServiceChannel);
const userDataSyncService = accessor.get(IUserDataSyncService); const userDataSyncService = accessor.get(IUserDataSyncService);
const userDataSyncChannel = new UserDataSyncChannel(userDataSyncService); const userDataSyncChannel = new UserDataSyncChannel(userDataSyncService);
server.registerChannel('userDataSync', userDataSyncChannel); server.registerChannel('userDataSync', userDataSyncChannel);

View File

@@ -30,6 +30,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { scrollbarShadow } from 'vs/platform/theme/common/colorRegistry'; import { scrollbarShadow } from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { Constants } from 'vs/base/common/uint';
const DIFF_LINES_PADDING = 3; const DIFF_LINES_PADDING = 3;
@@ -124,16 +125,6 @@ export class DiffReview extends Disposable {
} }
this._render(); this._render();
})); }));
this._register(diffEditor.getOriginalEditor().onDidFocusEditorWidget(() => {
if (this._isVisible) {
this.hide();
}
}));
this._register(diffEditor.getModifiedEditor().onDidFocusEditorWidget(() => {
if (this._isVisible) {
this.hide();
}
}));
this._register(dom.addStandardDisposableListener(this.domNode.domNode, 'click', (e) => { this._register(dom.addStandardDisposableListener(this.domNode.domNode, 'click', (e) => {
e.preventDefault(); e.preventDefault();
@@ -209,7 +200,9 @@ export class DiffReview extends Disposable {
} }
index = index % this._diffs.length; index = index % this._diffs.length;
this._diffEditor.setPosition(new Position(this._diffs[index].entries[0].modifiedLineStart, 1)); const entries = this._diffs[index].entries;
this._diffEditor.setPosition(new Position(entries[0].modifiedLineStart, 1));
this._diffEditor.setSelection({ startColumn: 1, startLineNumber: entries[0].modifiedLineStart, endColumn: Constants.MAX_SAFE_SMALL_INTEGER, endLineNumber: entries[entries.length - 1].modifiedLineEnd });
this._isVisible = true; this._isVisible = true;
this._diffEditor.doLayout(); this._diffEditor.doLayout();
this._render(); this._render();
@@ -242,7 +235,9 @@ export class DiffReview extends Disposable {
} }
index = index % this._diffs.length; index = index % this._diffs.length;
this._diffEditor.setPosition(new Position(this._diffs[index].entries[0].modifiedLineStart, 1)); const entries = this._diffs[index].entries;
this._diffEditor.setPosition(new Position(entries[0].modifiedLineStart, 1));
this._diffEditor.setSelection({ startColumn: 1, startLineNumber: entries[0].modifiedLineStart, endColumn: Constants.MAX_SAFE_SMALL_INTEGER, endLineNumber: entries[entries.length - 1].modifiedLineEnd });
this._isVisible = true; this._isVisible = true;
this._diffEditor.doLayout(); this._diffEditor.doLayout();
this._render(); this._render();
@@ -551,6 +546,7 @@ export class DiffReview extends Disposable {
let container = document.createElement('div'); let container = document.createElement('div');
container.className = 'diff-review-table'; container.className = 'diff-review-table';
container.setAttribute('role', 'list'); container.setAttribute('role', 'list');
container.setAttribute('aria-label', 'Difference review. Use "Stage | Unstage | Revert Selected Ranges" commands');
Configuration.applyFontInfoSlow(container, modifiedOptions.get(EditorOption.fontInfo)); Configuration.applyFontInfoSlow(container, modifiedOptions.get(EditorOption.fontInfo));
let minOriginalLine = 0; let minOriginalLine = 0;
@@ -590,11 +586,11 @@ export class DiffReview extends Disposable {
const getAriaLines = (lines: number) => { const getAriaLines = (lines: number) => {
if (lines === 0) { if (lines === 0) {
return nls.localize('no_lines', "no lines"); return nls.localize('no_lines_changed', "no lines changed");
} else if (lines === 1) { } else if (lines === 1) {
return nls.localize('one_line', "1 line"); return nls.localize('one_line_changed', "1 line changed");
} else { } else {
return nls.localize('more_lines', "{0} lines", lines); return nls.localize('more_lines_changed', "{0} lines changed", lines);
} }
}; };
@@ -608,9 +604,9 @@ export class DiffReview extends Disposable {
'That encodes that at original line 154 (which is now line 159), 12 lines were removed/changed with 39 lines.', 'That encodes that at original line 154 (which is now line 159), 12 lines were removed/changed with 39 lines.',
'Variables 0 and 1 refer to the diff index out of total number of diffs.', 'Variables 0 and 1 refer to the diff index out of total number of diffs.',
'Variables 2 and 4 will be numbers (a line number).', 'Variables 2 and 4 will be numbers (a line number).',
'Variables 3 and 5 will be "no lines", "1 line" or "X lines", localized separately.' 'Variables 3 and 5 will be "no lines changed", "1 line changed" or "X lines changed", localized separately.'
] ]
}, "Difference {0} of {1}: original {2}, {3}, modified {4}, {5}", (diffIndex + 1), this._diffs.length, minOriginalLine, originalChangedLinesCntAria, minModifiedLine, modifiedChangedLinesCntAria)); }, "Difference {0} of {1}: original line {2}, {3}, modified line {4}, {5}", (diffIndex + 1), this._diffs.length, minOriginalLine, originalChangedLinesCntAria, minModifiedLine, modifiedChangedLinesCntAria));
header.appendChild(cell); header.appendChild(cell);
// @@ -504,7 +517,7 @@ // @@ -504,7 +517,7 @@

View File

@@ -4,8 +4,13 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as strings from 'vs/base/common/strings'; import * as strings from 'vs/base/common/strings';
import * as platform from 'vs/base/common/platform';
import * as buffer from 'vs/base/common/buffer';
declare const TextDecoder: any; // TODO@TypeScript declare const TextDecoder: {
prototype: TextDecoder;
new(label?: string): TextDecoder;
};
interface TextDecoder { interface TextDecoder {
decode(view: Uint16Array): string; decode(view: Uint16Array): string;
} }
@@ -18,17 +23,42 @@ export interface IStringBuilder {
appendASCIIString(str: string): void; appendASCIIString(str: string): void;
} }
let _platformTextDecoder: TextDecoder | null;
function getPlatformTextDecoder(): TextDecoder {
if (!_platformTextDecoder) {
_platformTextDecoder = new TextDecoder(platform.isLittleEndian() ? 'UTF-16LE' : 'UTF-16BE');
}
return _platformTextDecoder;
}
export let createStringBuilder: (capacity: number) => IStringBuilder; export let createStringBuilder: (capacity: number) => IStringBuilder;
export let decodeUTF16LE: (source: Uint8Array, offset: number, len: number) => string;
if (typeof TextDecoder !== 'undefined') { if (typeof TextDecoder !== 'undefined') {
createStringBuilder = (capacity) => new StringBuilder(capacity); createStringBuilder = (capacity) => new StringBuilder(capacity);
decodeUTF16LE = standardDecodeUTF16LE;
} else { } else {
createStringBuilder = (capacity) => new CompatStringBuilder(); createStringBuilder = (capacity) => new CompatStringBuilder();
decodeUTF16LE = compatDecodeUTF16LE;
}
function standardDecodeUTF16LE(source: Uint8Array, offset: number, len: number): string {
const view = new Uint16Array(source.buffer, offset, len);
return getPlatformTextDecoder().decode(view);
}
function compatDecodeUTF16LE(source: Uint8Array, offset: number, len: number): string {
let result: string[] = [];
let resultLen = 0;
for (let i = 0; i < len; i++) {
const charCode = buffer.readUInt16LE(source, offset); offset += 2;
result[resultLen++] = String.fromCharCode(charCode);
}
return result.join('');
} }
class StringBuilder implements IStringBuilder { class StringBuilder implements IStringBuilder {
private readonly _decoder: TextDecoder;
private readonly _capacity: number; private readonly _capacity: number;
private readonly _buffer: Uint16Array; private readonly _buffer: Uint16Array;
@@ -36,7 +66,6 @@ class StringBuilder implements IStringBuilder {
private _bufferLength: number; private _bufferLength: number;
constructor(capacity: number) { constructor(capacity: number) {
this._decoder = new TextDecoder('UTF-16LE');
this._capacity = capacity | 0; this._capacity = capacity | 0;
this._buffer = new Uint16Array(this._capacity); this._buffer = new Uint16Array(this._capacity);
@@ -63,7 +92,7 @@ class StringBuilder implements IStringBuilder {
} }
const view = new Uint16Array(this._buffer.buffer, 0, this._bufferLength); const view = new Uint16Array(this._buffer.buffer, 0, this._bufferLength);
return this._decoder.decode(view); return getPlatformTextDecoder().decode(view);
} }
private _flushBuffer(): void { private _flushBuffer(): void {

View File

@@ -15,6 +15,7 @@ import { SearchData } from 'vs/editor/common/model/textModelSearch';
import { LanguageId, LanguageIdentifier, FormattingOptions } from 'vs/editor/common/modes'; import { LanguageId, LanguageIdentifier, FormattingOptions } from 'vs/editor/common/modes';
import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { ThemeColor } from 'vs/platform/theme/common/themeService';
import { MultilineTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore'; import { MultilineTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore';
import { TextChange } from 'vs/editor/common/model/textChange';
/** /**
* Vertical Lane in the overview ruler of the editor. * Vertical Lane in the overview ruler of the editor.
@@ -373,21 +374,13 @@ export interface IValidEditOperation {
*/ */
range: Range; range: Range;
/** /**
* The text to replace with. This can be null to emulate a simple delete. * The text to replace with. This can be empty to emulate a simple delete.
*/ */
text: string | null; text: string;
/** /**
* This indicates that this operation has "insert" semantics. * @internal
* i.e. forceMoveMarkers = true => if `range` is collapsed, all markers at the position will be moved.
*/ */
forceMoveMarkers: boolean; textChange: TextChange;
}
/**
* @internal
*/
export interface IValidEditOperations {
operations: IValidEditOperation[];
} }
/** /**
@@ -1099,9 +1092,11 @@ export interface ITextModel {
* Edit the model without adding the edits to the undo stack. * Edit the model without adding the edits to the undo stack.
* This can have dire consequences on the undo stack! See @pushEditOperations for the preferred way. * This can have dire consequences on the undo stack! See @pushEditOperations for the preferred way.
* @param operations The edit operations. * @param operations The edit operations.
* @return The inverse edit operations, that, when applied, will bring the model back to the previous state. * @return If desired, the inverse edit operations, that, when applied, will bring the model back to the previous state.
*/ */
applyEdits(operations: IIdentifiedSingleEditOperation[]): IValidEditOperation[]; applyEdits(operations: IIdentifiedSingleEditOperation[]): void;
applyEdits(operations: IIdentifiedSingleEditOperation[], computeUndoEdits: false): void;
applyEdits(operations: IIdentifiedSingleEditOperation[], computeUndoEdits: true): IValidEditOperation[];
/** /**
* Change the end of line sequence without recording in the undo stack. * Change the end of line sequence without recording in the undo stack.
@@ -1112,7 +1107,12 @@ export interface ITextModel {
/** /**
* @internal * @internal
*/ */
_applyUndoRedoEdits(edits: IValidEditOperations[], eol: EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): IValidEditOperations[]; _applyUndo(changes: TextChange[], eol: EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void;
/**
* @internal
*/
_applyRedo(changes: TextChange[], eol: EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void;
/** /**
* Undo edit operations until the first previous stop point created by `pushStackElement`. * Undo edit operations until the first previous stop point created by `pushStackElement`.
@@ -1291,7 +1291,7 @@ export interface ITextBuffer {
getLineLastNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number;
setEOL(newEOL: '\r\n' | '\n'): void; setEOL(newEOL: '\r\n' | '\n'): void;
applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult; applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean, computeUndoEdits: boolean): ApplyEditsResult;
findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[]; findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[];
} }
@@ -1301,7 +1301,7 @@ export interface ITextBuffer {
export class ApplyEditsResult { export class ApplyEditsResult {
constructor( constructor(
public readonly reverseEdits: IValidEditOperation[], public readonly reverseEdits: IValidEditOperation[] | null,
public readonly changes: IInternalModelContentChange[], public readonly changes: IInternalModelContentChange[],
public readonly trimAutoWhitespaceLineNumbers: number[] | null public readonly trimAutoWhitespaceLineNumbers: number[] | null
) { } ) { }

View File

@@ -6,69 +6,209 @@
import * as nls from 'vs/nls'; import * as nls from 'vs/nls';
import { onUnexpectedError } from 'vs/base/common/errors'; import { onUnexpectedError } from 'vs/base/common/errors';
import { Selection } from 'vs/editor/common/core/selection'; import { Selection } from 'vs/editor/common/core/selection';
import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation, ITextModel, IValidEditOperations } from 'vs/editor/common/model'; import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation, ITextModel } from 'vs/editor/common/model';
import { TextModel } from 'vs/editor/common/model/textModel'; import { TextModel } from 'vs/editor/common/model/textModel';
import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources'; import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources';
import { TextChange, compressConsecutiveTextChanges } from 'vs/editor/common/model/textChange';
import * as buffer from 'vs/base/common/buffer';
export class EditStackElement implements IResourceUndoRedoElement { class SingleModelEditStackData {
public readonly type = UndoRedoElementType.Resource; public static create(model: ITextModel, beforeCursorState: Selection[] | null): SingleModelEditStackData {
public readonly label: string; const alternativeVersionId = model.getAlternativeVersionId();
private _isOpen: boolean; const eol = getModelEOL(model);
public readonly model: ITextModel; return new SingleModelEditStackData(
private readonly _beforeVersionId: number; alternativeVersionId,
private readonly _beforeEOL: EndOfLineSequence; alternativeVersionId,
private readonly _beforeCursorState: Selection[] | null; eol,
private _afterVersionId: number; eol,
private _afterEOL: EndOfLineSequence; beforeCursorState,
private _afterCursorState: Selection[] | null; beforeCursorState,
private _edits: IValidEditOperations[]; []
);
public get resource(): URI {
return this.model.uri;
} }
constructor(model: ITextModel, beforeCursorState: Selection[] | null) { constructor(
this.label = nls.localize('edit', "Typing"); public readonly beforeVersionId: number,
this._isOpen = true; public afterVersionId: number,
this.model = model; public readonly beforeEOL: EndOfLineSequence,
this._beforeVersionId = this.model.getAlternativeVersionId(); public afterEOL: EndOfLineSequence,
this._beforeEOL = getModelEOL(this.model); public readonly beforeCursorState: Selection[] | null,
this._beforeCursorState = beforeCursorState; public afterCursorState: Selection[] | null,
this._afterVersionId = this._beforeVersionId; public changes: TextChange[]
this._afterEOL = this._beforeEOL; ) { }
this._afterCursorState = this._beforeCursorState;
this._edits = [];
}
public canAppend(model: ITextModel): boolean {
return (this._isOpen && this.model === model);
}
public append(model: ITextModel, operations: IValidEditOperation[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void { public append(model: ITextModel, operations: IValidEditOperation[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {
if (operations.length > 0) { if (operations.length > 0) {
this._edits.push({ operations: operations }); this.changes = compressConsecutiveTextChanges(this.changes, operations.map(op => op.textChange));
}
this.afterEOL = afterEOL;
this.afterVersionId = afterVersionId;
this.afterCursorState = afterCursorState;
}
private static _writeSelectionsSize(selections: Selection[] | null): number {
return 4 + 4 * 4 * (selections ? selections.length : 0);
}
private static _writeSelections(b: Uint8Array, selections: Selection[] | null, offset: number): number {
buffer.writeUInt32BE(b, (selections ? selections.length : 0), offset); offset += 4;
if (selections) {
for (const selection of selections) {
buffer.writeUInt32BE(b, selection.selectionStartLineNumber, offset); offset += 4;
buffer.writeUInt32BE(b, selection.selectionStartColumn, offset); offset += 4;
buffer.writeUInt32BE(b, selection.positionLineNumber, offset); offset += 4;
buffer.writeUInt32BE(b, selection.positionColumn, offset); offset += 4;
}
}
return offset;
}
private static _readSelections(b: Uint8Array, offset: number, dest: Selection[]): number {
const count = buffer.readUInt32BE(b, offset); offset += 4;
for (let i = 0; i < count; i++) {
const selectionStartLineNumber = buffer.readUInt32BE(b, offset); offset += 4;
const selectionStartColumn = buffer.readUInt32BE(b, offset); offset += 4;
const positionLineNumber = buffer.readUInt32BE(b, offset); offset += 4;
const positionColumn = buffer.readUInt32BE(b, offset); offset += 4;
dest.push(new Selection(selectionStartLineNumber, selectionStartColumn, positionLineNumber, positionColumn));
}
return offset;
}
public serialize(): ArrayBuffer {
let necessarySize = (
+ 4 // beforeVersionId
+ 4 // afterVersionId
+ 1 // beforeEOL
+ 1 // afterEOL
+ SingleModelEditStackData._writeSelectionsSize(this.beforeCursorState)
+ SingleModelEditStackData._writeSelectionsSize(this.afterCursorState)
+ 4 // change count
);
for (const change of this.changes) {
necessarySize += change.writeSize();
}
const b = new Uint8Array(necessarySize);
let offset = 0;
buffer.writeUInt32BE(b, this.beforeVersionId, offset); offset += 4;
buffer.writeUInt32BE(b, this.afterVersionId, offset); offset += 4;
buffer.writeUInt8(b, this.beforeEOL, offset); offset += 1;
buffer.writeUInt8(b, this.afterEOL, offset); offset += 1;
offset = SingleModelEditStackData._writeSelections(b, this.beforeCursorState, offset);
offset = SingleModelEditStackData._writeSelections(b, this.afterCursorState, offset);
buffer.writeUInt32BE(b, this.changes.length, offset); offset += 4;
for (const change of this.changes) {
offset = change.write(b, offset);
}
return b.buffer;
}
public static deserialize(source: ArrayBuffer): SingleModelEditStackData {
const b = new Uint8Array(source);
let offset = 0;
const beforeVersionId = buffer.readUInt32BE(b, offset); offset += 4;
const afterVersionId = buffer.readUInt32BE(b, offset); offset += 4;
const beforeEOL = buffer.readUInt8(b, offset); offset += 1;
const afterEOL = buffer.readUInt8(b, offset); offset += 1;
const beforeCursorState: Selection[] = [];
offset = SingleModelEditStackData._readSelections(b, offset, beforeCursorState);
const afterCursorState: Selection[] = [];
offset = SingleModelEditStackData._readSelections(b, offset, afterCursorState);
const changeCount = buffer.readUInt32BE(b, offset); offset += 4;
const changes: TextChange[] = [];
for (let i = 0; i < changeCount; i++) {
offset = TextChange.read(b, offset, changes);
}
return new SingleModelEditStackData(
beforeVersionId,
afterVersionId,
beforeEOL,
afterEOL,
beforeCursorState,
afterCursorState,
changes
);
}
}
export class SingleModelEditStackElement implements IResourceUndoRedoElement {
public model: ITextModel | URI;
private _data: SingleModelEditStackData | ArrayBuffer;
public get type(): UndoRedoElementType.Resource {
return UndoRedoElementType.Resource;
}
public get resource(): URI {
if (URI.isUri(this.model)) {
return this.model;
}
return this.model.uri;
}
public get label(): string {
return nls.localize('edit', "Typing");
}
constructor(model: ITextModel, beforeCursorState: Selection[] | null) {
this.model = model;
this._data = SingleModelEditStackData.create(model, beforeCursorState);
}
public setModel(model: ITextModel | URI): void {
this.model = model;
}
public canAppend(model: ITextModel): boolean {
return (this.model === model && this._data instanceof SingleModelEditStackData);
}
public append(model: ITextModel, operations: IValidEditOperation[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {
if (this._data instanceof SingleModelEditStackData) {
this._data.append(model, operations, afterEOL, afterVersionId, afterCursorState);
} }
this._afterEOL = afterEOL;
this._afterVersionId = afterVersionId;
this._afterCursorState = afterCursorState;
} }
public close(): void { public close(): void {
this._isOpen = false; if (this._data instanceof SingleModelEditStackData) {
this._data = this._data.serialize();
}
} }
public undo(): void { public undo(): void {
this._isOpen = false; if (URI.isUri(this.model)) {
this._edits.reverse(); // don't have a model
this._edits = this.model._applyUndoRedoEdits(this._edits, this._beforeEOL, true, false, this._beforeVersionId, this._beforeCursorState); throw new Error(`Invalid SingleModelEditStackElement`);
}
if (this._data instanceof SingleModelEditStackData) {
this._data = this._data.serialize();
}
const data = SingleModelEditStackData.deserialize(this._data);
this.model._applyUndo(data.changes, data.beforeEOL, data.beforeVersionId, data.beforeCursorState);
} }
public redo(): void { public redo(): void {
this._edits.reverse(); if (URI.isUri(this.model)) {
this._edits = this.model._applyUndoRedoEdits(this._edits, this._afterEOL, false, true, this._afterVersionId, this._afterCursorState); // don't have a model
throw new Error(`Invalid SingleModelEditStackElement`);
}
if (this._data instanceof SingleModelEditStackData) {
this._data = this._data.serialize();
}
const data = SingleModelEditStackData.deserialize(this._data);
this.model._applyRedo(data.changes, data.afterEOL, data.afterVersionId, data.afterCursorState);
}
public heapSize(): number {
if (this._data instanceof SingleModelEditStackData) {
this._data = this._data.serialize();
}
return this._data.byteLength + 168/*heap overhead*/;
} }
} }
@@ -78,27 +218,34 @@ export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement {
public readonly label: string; public readonly label: string;
private _isOpen: boolean; private _isOpen: boolean;
private readonly _editStackElementsArr: EditStackElement[]; private readonly _editStackElementsArr: SingleModelEditStackElement[];
private readonly _editStackElementsMap: Map<string, EditStackElement>; private readonly _editStackElementsMap: Map<string, SingleModelEditStackElement>;
public get resources(): readonly URI[] { public get resources(): readonly URI[] {
return this._editStackElementsArr.map(editStackElement => editStackElement.model.uri); return this._editStackElementsArr.map(editStackElement => editStackElement.resource);
} }
constructor( constructor(
label: string, label: string,
editStackElements: EditStackElement[] editStackElements: SingleModelEditStackElement[]
) { ) {
this.label = label; this.label = label;
this._isOpen = true; this._isOpen = true;
this._editStackElementsArr = editStackElements.slice(0); this._editStackElementsArr = editStackElements.slice(0);
this._editStackElementsMap = new Map<string, EditStackElement>(); this._editStackElementsMap = new Map<string, SingleModelEditStackElement>();
for (const editStackElement of this._editStackElementsArr) { for (const editStackElement of this._editStackElementsArr) {
const key = uriGetComparisonKey(editStackElement.model.uri); const key = uriGetComparisonKey(editStackElement.resource);
this._editStackElementsMap.set(key, editStackElement); this._editStackElementsMap.set(key, editStackElement);
} }
} }
public setModel(model: ITextModel | URI): void {
const key = uriGetComparisonKey(URI.isUri(model) ? model : model.uri);
if (this._editStackElementsMap.has(key)) {
this._editStackElementsMap.get(key)!.setModel(model);
}
}
public canAppend(model: ITextModel): boolean { public canAppend(model: ITextModel): boolean {
if (!this._isOpen) { if (!this._isOpen) {
return false; return false;
@@ -135,11 +282,22 @@ export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement {
} }
} }
public heapSize(resource: URI): number {
const key = uriGetComparisonKey(resource);
if (this._editStackElementsMap.has(key)) {
const editStackElement = this._editStackElementsMap.get(key)!;
return editStackElement.heapSize();
}
return 0;
}
public split(): IResourceUndoRedoElement[] { public split(): IResourceUndoRedoElement[] {
return this._editStackElementsArr; return this._editStackElementsArr;
} }
} }
export type EditStackElement = SingleModelEditStackElement | MultiModelEditStackElement;
function getModelEOL(model: ITextModel): EndOfLineSequence { function getModelEOL(model: ITextModel): EndOfLineSequence {
const eol = model.getEOL(); const eol = model.getEOL();
if (eol === '\n') { if (eol === '\n') {
@@ -149,11 +307,11 @@ function getModelEOL(model: ITextModel): EndOfLineSequence {
} }
} }
function isKnownStackElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null): element is EditStackElement | MultiModelEditStackElement { function isKnownStackElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null): element is EditStackElement {
if (!element) { if (!element) {
return false; return false;
} }
return ((element instanceof EditStackElement) || (element instanceof MultiModelEditStackElement)); return ((element instanceof SingleModelEditStackElement) || (element instanceof MultiModelEditStackElement));
} }
export class EditStack { export class EditStack {
@@ -177,12 +335,12 @@ export class EditStack {
this._undoRedoService.removeElements(this._model.uri); this._undoRedoService.removeElements(this._model.uri);
} }
private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null): EditStackElement | MultiModelEditStackElement { private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null): EditStackElement {
const lastElement = this._undoRedoService.getLastElement(this._model.uri); const lastElement = this._undoRedoService.getLastElement(this._model.uri);
if (isKnownStackElement(lastElement) && lastElement.canAppend(this._model)) { if (isKnownStackElement(lastElement) && lastElement.canAppend(this._model)) {
return lastElement; return lastElement;
} }
const newElement = new EditStackElement(this._model, beforeCursorState); const newElement = new SingleModelEditStackElement(this._model, beforeCursorState);
this._undoRedoService.pushElement(newElement); this._undoRedoService.pushElement(newElement);
return newElement; return newElement;
} }
@@ -195,7 +353,7 @@ export class EditStack {
public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer | null): Selection[] | null { public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer | null): Selection[] | null {
const editStackElement = this._getOrCreateEditStackElement(beforeCursorState); const editStackElement = this._getOrCreateEditStackElement(beforeCursorState);
const inverseEditOperations = this._model.applyEdits(editOperations); const inverseEditOperations = this._model.applyEdits(editOperations, true);
const afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperations); const afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperations);
editStackElement.append(this._model, inverseEditOperations, getModelEOL(this._model), this._model.getAlternativeVersionId(), afterCursorState); editStackElement.append(this._model, inverseEditOperations, getModelEOL(this._model), this._model.getAlternativeVersionId(), afterCursorState);
return afterCursorState; return afterCursorState;

View File

@@ -269,7 +269,7 @@ export class PieceTreeBase {
protected _buffers!: StringBuffer[]; // 0 is change buffer, others are readonly original buffer. protected _buffers!: StringBuffer[]; // 0 is change buffer, others are readonly original buffer.
protected _lineCnt!: number; protected _lineCnt!: number;
protected _length!: number; protected _length!: number;
protected _EOL!: string; protected _EOL!: '\r\n' | '\n';
protected _EOLLength!: number; protected _EOLLength!: number;
protected _EOLNormalized!: boolean; protected _EOLNormalized!: boolean;
private _lastChangeBufferPos!: BufferCursor; private _lastChangeBufferPos!: BufferCursor;
@@ -351,7 +351,7 @@ export class PieceTreeBase {
} }
// #region Buffer API // #region Buffer API
public getEOL(): string { public getEOL(): '\r\n' | '\n' {
return this._EOL; return this._EOL;
} }

View File

@@ -9,6 +9,8 @@ import { Range } from 'vs/editor/common/core/range';
import { ApplyEditsResult, EndOfLinePreference, FindMatch, IInternalModelContentChange, ISingleEditOperationIdentifier, ITextBuffer, ITextSnapshot, ValidAnnotatedEditOperation, IValidEditOperation } from 'vs/editor/common/model'; import { ApplyEditsResult, EndOfLinePreference, FindMatch, IInternalModelContentChange, ISingleEditOperationIdentifier, ITextBuffer, ITextSnapshot, ValidAnnotatedEditOperation, IValidEditOperation } from 'vs/editor/common/model';
import { PieceTreeBase, StringBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; import { PieceTreeBase, StringBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase';
import { SearchData } from 'vs/editor/common/model/textModelSearch'; import { SearchData } from 'vs/editor/common/model/textModelSearch';
import { countEOL, StringEOL } from 'vs/editor/common/model/tokensStore';
import { TextChange } from 'vs/editor/common/model/textChange';
export interface IValidatedEditOperation { export interface IValidatedEditOperation {
sortIndex: number; sortIndex: number;
@@ -16,7 +18,10 @@ export interface IValidatedEditOperation {
range: Range; range: Range;
rangeOffset: number; rangeOffset: number;
rangeLength: number; rangeLength: number;
lines: string[] | null; text: string;
eolCount: number;
firstLineLength: number;
lastLineLength: number;
forceMoveMarkers: boolean; forceMoveMarkers: boolean;
isAutoWhitespaceEdit: boolean; isAutoWhitespaceEdit: boolean;
} }
@@ -60,7 +65,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
public getBOM(): string { public getBOM(): string {
return this._BOM; return this._BOM;
} }
public getEOL(): string { public getEOL(): '\r\n' | '\n' {
return this._pieceTree.getEOL(); return this._pieceTree.getEOL();
} }
@@ -201,7 +206,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
this._pieceTree.setEOL(newEOL); this._pieceTree.setEOL(newEOL);
} }
public applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult { public applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean, computeUndoEdits: boolean): ApplyEditsResult {
let mightContainRTL = this._mightContainRTL; let mightContainRTL = this._mightContainRTL;
let mightContainNonBasicASCII = this._mightContainNonBasicASCII; let mightContainNonBasicASCII = this._mightContainNonBasicASCII;
let canReduceOperations = true; let canReduceOperations = true;
@@ -220,13 +225,34 @@ export class PieceTreeTextBuffer implements ITextBuffer {
if (!mightContainNonBasicASCII && op.text) { if (!mightContainNonBasicASCII && op.text) {
mightContainNonBasicASCII = !strings.isBasicASCII(op.text); mightContainNonBasicASCII = !strings.isBasicASCII(op.text);
} }
let validText = '';
let eolCount = 0;
let firstLineLength = 0;
let lastLineLength = 0;
if (op.text) {
let strEOL: StringEOL;
[eolCount, firstLineLength, lastLineLength, strEOL] = countEOL(op.text);
const bufferEOL = this.getEOL();
const expectedStrEOL = (bufferEOL === '\r\n' ? StringEOL.CRLF : StringEOL.LF);
if (strEOL === StringEOL.Unknown || strEOL === expectedStrEOL) {
validText = op.text;
} else {
validText = op.text.replace(/\r\n|\r|\n/g, bufferEOL);
}
}
operations[i] = { operations[i] = {
sortIndex: i, sortIndex: i,
identifier: op.identifier || null, identifier: op.identifier || null,
range: validatedRange, range: validatedRange,
rangeOffset: this.getOffsetAt(validatedRange.startLineNumber, validatedRange.startColumn), rangeOffset: this.getOffsetAt(validatedRange.startLineNumber, validatedRange.startColumn),
rangeLength: this.getValueLengthInRange(validatedRange), rangeLength: this.getValueLengthInRange(validatedRange),
lines: op.text ? op.text.split(/\r\n|\r|\n/) : null, text: validText,
eolCount: eolCount,
firstLineLength: firstLineLength,
lastLineLength: lastLineLength,
forceMoveMarkers: Boolean(op.forceMoveMarkers), forceMoveMarkers: Boolean(op.forceMoveMarkers),
isAutoWhitespaceEdit: op.isAutoWhitespaceEdit || false isAutoWhitespaceEdit: op.isAutoWhitespaceEdit || false
}; };
@@ -254,46 +280,56 @@ export class PieceTreeTextBuffer implements ITextBuffer {
} }
// Delta encode operations // Delta encode operations
let reverseRanges = PieceTreeTextBuffer._getInverseEditRanges(operations); let reverseRanges = (computeUndoEdits || recordTrimAutoWhitespace ? PieceTreeTextBuffer._getInverseEditRanges(operations) : []);
let newTrimAutoWhitespaceCandidates: { lineNumber: number, oldContent: string }[] = []; let newTrimAutoWhitespaceCandidates: { lineNumber: number, oldContent: string }[] = [];
if (recordTrimAutoWhitespace) {
for (let i = 0; i < operations.length; i++) {
let op = operations[i];
let reverseRange = reverseRanges[i];
for (let i = 0; i < operations.length; i++) { if (op.isAutoWhitespaceEdit && op.range.isEmpty()) {
let op = operations[i]; // Record already the future line numbers that might be auto whitespace removal candidates on next edit
let reverseRange = reverseRanges[i]; for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) {
let currentLineContent = '';
if (recordTrimAutoWhitespace && op.isAutoWhitespaceEdit && op.range.isEmpty()) { if (lineNumber === reverseRange.startLineNumber) {
// Record already the future line numbers that might be auto whitespace removal candidates on next edit currentLineContent = this.getLineContent(op.range.startLineNumber);
for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) { if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) {
let currentLineContent = ''; continue;
if (lineNumber === reverseRange.startLineNumber) { }
currentLineContent = this.getLineContent(op.range.startLineNumber);
if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) {
continue;
} }
newTrimAutoWhitespaceCandidates.push({ lineNumber: lineNumber, oldContent: currentLineContent });
} }
newTrimAutoWhitespaceCandidates.push({ lineNumber: lineNumber, oldContent: currentLineContent });
} }
} }
} }
let reverseOperations: IReverseSingleEditOperation[] = []; let reverseOperations: IReverseSingleEditOperation[] | null = null;
for (let i = 0; i < operations.length; i++) { if (computeUndoEdits) {
let op = operations[i];
let reverseRange = reverseRanges[i];
reverseOperations[i] = { let reverseRangeDeltaOffset = 0;
sortIndex: op.sortIndex, reverseOperations = [];
identifier: op.identifier, for (let i = 0; i < operations.length; i++) {
range: reverseRange, const op = operations[i];
text: this.getValueInRange(op.range), const reverseRange = reverseRanges[i];
forceMoveMarkers: op.forceMoveMarkers const bufferText = this.getValueInRange(op.range);
}; const reverseRangeOffset = op.rangeOffset + reverseRangeDeltaOffset;
reverseRangeDeltaOffset += (op.text.length - bufferText.length);
reverseOperations[i] = {
sortIndex: op.sortIndex,
identifier: op.identifier,
range: reverseRange,
text: bufferText,
textChange: new TextChange(op.rangeOffset, bufferText, reverseRangeOffset, op.text)
};
}
// Can only sort reverse operations when the order is not significant
if (!hasTouchingRanges) {
reverseOperations.sort((a, b) => a.sortIndex - b.sortIndex);
}
} }
// Can only sort reverse operations when the order is not significant
if (!hasTouchingRanges) {
reverseOperations.sort((a, b) => a.sortIndex - b.sortIndex);
}
this._mightContainRTL = mightContainRTL; this._mightContainRTL = mightContainRTL;
this._mightContainNonBasicASCII = mightContainNonBasicASCII; this._mightContainNonBasicASCII = mightContainNonBasicASCII;
@@ -350,58 +386,45 @@ export class PieceTreeTextBuffer implements ITextBuffer {
} }
_toSingleEditOperation(operations: IValidatedEditOperation[]): IValidatedEditOperation { _toSingleEditOperation(operations: IValidatedEditOperation[]): IValidatedEditOperation {
let forceMoveMarkers = false, let forceMoveMarkers = false;
firstEditRange = operations[0].range, const firstEditRange = operations[0].range;
lastEditRange = operations[operations.length - 1].range, const lastEditRange = operations[operations.length - 1].range;
entireEditRange = new Range(firstEditRange.startLineNumber, firstEditRange.startColumn, lastEditRange.endLineNumber, lastEditRange.endColumn), const entireEditRange = new Range(firstEditRange.startLineNumber, firstEditRange.startColumn, lastEditRange.endLineNumber, lastEditRange.endColumn);
lastEndLineNumber = firstEditRange.startLineNumber, let lastEndLineNumber = firstEditRange.startLineNumber;
lastEndColumn = firstEditRange.startColumn, let lastEndColumn = firstEditRange.startColumn;
result: string[] = []; const result: string[] = [];
for (let i = 0, len = operations.length; i < len; i++) { for (let i = 0, len = operations.length; i < len; i++) {
let operation = operations[i], const operation = operations[i];
range = operation.range; const range = operation.range;
forceMoveMarkers = forceMoveMarkers || operation.forceMoveMarkers; forceMoveMarkers = forceMoveMarkers || operation.forceMoveMarkers;
// (1) -- Push old text // (1) -- Push old text
for (let lineNumber = lastEndLineNumber; lineNumber < range.startLineNumber; lineNumber++) { result.push(this.getValueInRange(new Range(lastEndLineNumber, lastEndColumn, range.startLineNumber, range.startColumn)));
if (lineNumber === lastEndLineNumber) {
result.push(this.getLineContent(lineNumber).substring(lastEndColumn - 1));
} else {
result.push('\n');
result.push(this.getLineContent(lineNumber));
}
}
if (range.startLineNumber === lastEndLineNumber) {
result.push(this.getLineContent(range.startLineNumber).substring(lastEndColumn - 1, range.startColumn - 1));
} else {
result.push('\n');
result.push(this.getLineContent(range.startLineNumber).substring(0, range.startColumn - 1));
}
// (2) -- Push new text // (2) -- Push new text
if (operation.lines) { if (operation.text.length > 0) {
for (let j = 0, lenJ = operation.lines.length; j < lenJ; j++) { result.push(operation.text);
if (j !== 0) {
result.push('\n');
}
result.push(operation.lines[j]);
}
} }
lastEndLineNumber = operation.range.endLineNumber; lastEndLineNumber = range.endLineNumber;
lastEndColumn = operation.range.endColumn; lastEndColumn = range.endColumn;
} }
const text = result.join('');
const [eolCount, firstLineLength, lastLineLength] = countEOL(text);
return { return {
sortIndex: 0, sortIndex: 0,
identifier: operations[0].identifier, identifier: operations[0].identifier,
range: entireEditRange, range: entireEditRange,
rangeOffset: this.getOffsetAt(entireEditRange.startLineNumber, entireEditRange.startColumn), rangeOffset: this.getOffsetAt(entireEditRange.startLineNumber, entireEditRange.startColumn),
rangeLength: this.getValueLengthInRange(entireEditRange, EndOfLinePreference.TextDefined), rangeLength: this.getValueLengthInRange(entireEditRange, EndOfLinePreference.TextDefined),
lines: result.join('').split('\n'), text: text,
eolCount: eolCount,
firstLineLength: firstLineLength,
lastLineLength: lastLineLength,
forceMoveMarkers: forceMoveMarkers, forceMoveMarkers: forceMoveMarkers,
isAutoWhitespaceEdit: false isAutoWhitespaceEdit: false
}; };
@@ -421,41 +444,26 @@ export class PieceTreeTextBuffer implements ITextBuffer {
const endLineNumber = op.range.endLineNumber; const endLineNumber = op.range.endLineNumber;
const endColumn = op.range.endColumn; const endColumn = op.range.endColumn;
if (startLineNumber === endLineNumber && startColumn === endColumn && (!op.lines || op.lines.length === 0)) { if (startLineNumber === endLineNumber && startColumn === endColumn && op.text.length === 0) {
// no-op // no-op
continue; continue;
} }
const deletingLinesCnt = endLineNumber - startLineNumber; if (op.text) {
const insertingLinesCnt = (op.lines ? op.lines.length - 1 : 0);
const editingLinesCnt = Math.min(deletingLinesCnt, insertingLinesCnt);
const text = (op.lines ? op.lines.join(this.getEOL()) : '');
if (text) {
// replacement // replacement
this._pieceTree.delete(op.rangeOffset, op.rangeLength); this._pieceTree.delete(op.rangeOffset, op.rangeLength);
this._pieceTree.insert(op.rangeOffset, text, true); this._pieceTree.insert(op.rangeOffset, op.text, true);
} else { } else {
// deletion // deletion
this._pieceTree.delete(op.rangeOffset, op.rangeLength); this._pieceTree.delete(op.rangeOffset, op.rangeLength);
} }
if (editingLinesCnt < insertingLinesCnt) {
let newLinesContent: string[] = [];
for (let j = editingLinesCnt + 1; j <= insertingLinesCnt; j++) {
newLinesContent.push(op.lines![j]);
}
newLinesContent[newLinesContent.length - 1] = this.getLineContent(startLineNumber + insertingLinesCnt - 1);
}
const contentChangeRange = new Range(startLineNumber, startColumn, endLineNumber, endColumn); const contentChangeRange = new Range(startLineNumber, startColumn, endLineNumber, endColumn);
contentChanges.push({ contentChanges.push({
range: contentChangeRange, range: contentChangeRange,
rangeLength: op.rangeLength, rangeLength: op.rangeLength,
text: text, text: op.text,
rangeOffset: op.rangeOffset, rangeOffset: op.rangeOffset,
forceMoveMarkers: op.forceMoveMarkers forceMoveMarkers: op.forceMoveMarkers
}); });
@@ -504,18 +512,16 @@ export class PieceTreeTextBuffer implements ITextBuffer {
let resultRange: Range; let resultRange: Range;
if (op.lines && op.lines.length > 0) { if (op.text.length > 0) {
// the operation inserts something // the operation inserts something
let lineCount = op.lines.length; const lineCount = op.eolCount + 1;
let firstLine = op.lines[0];
let lastLine = op.lines[lineCount - 1];
if (lineCount === 1) { if (lineCount === 1) {
// single line insert // single line insert
resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + firstLine.length); resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + op.firstLineLength);
} else { } else {
// multi line insert // multi line insert
resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, lastLine.length + 1); resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, op.lastLineLength + 1);
} }
} else { } else {
// There is nothing to insert // There is nothing to insert

View File

@@ -0,0 +1,326 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as buffer from 'vs/base/common/buffer';
import { decodeUTF16LE } from 'vs/editor/common/core/stringBuilder';
export class TextChange {
public get oldLength(): number {
return this.oldText.length;
}
public get oldEnd(): number {
return this.oldPosition + this.oldText.length;
}
public get newLength(): number {
return this.newText.length;
}
public get newEnd(): number {
return this.newPosition + this.newText.length;
}
constructor(
public readonly oldPosition: number,
public readonly oldText: string,
public readonly newPosition: number,
public readonly newText: string
) { }
private static _writeStringSize(str: string): number {
return (
4 + 2 * str.length
);
}
private static _writeString(b: Uint8Array, str: string, offset: number): number {
const len = str.length;
buffer.writeUInt32BE(b, len, offset); offset += 4;
for (let i = 0; i < len; i++) {
buffer.writeUInt16LE(b, str.charCodeAt(i), offset); offset += 2;
}
return offset;
}
private static _readString(b: Uint8Array, offset: number): string {
const len = buffer.readUInt32BE(b, offset); offset += 4;
return decodeUTF16LE(b, offset, len);
}
public writeSize(): number {
return (
+ 4 // oldPosition
+ 4 // newPosition
+ TextChange._writeStringSize(this.oldText)
+ TextChange._writeStringSize(this.newText)
);
}
public write(b: Uint8Array, offset: number): number {
buffer.writeUInt32BE(b, this.oldPosition, offset); offset += 4;
buffer.writeUInt32BE(b, this.newPosition, offset); offset += 4;
offset = TextChange._writeString(b, this.oldText, offset);
offset = TextChange._writeString(b, this.newText, offset);
return offset;
}
public static read(b: Uint8Array, offset: number, dest: TextChange[]): number {
const oldPosition = buffer.readUInt32BE(b, offset); offset += 4;
const newPosition = buffer.readUInt32BE(b, offset); offset += 4;
const oldText = TextChange._readString(b, offset); offset += TextChange._writeStringSize(oldText);
const newText = TextChange._readString(b, offset); offset += TextChange._writeStringSize(newText);
dest.push(new TextChange(oldPosition, oldText, newPosition, newText));
return offset;
}
}
export function compressConsecutiveTextChanges(prevEdits: TextChange[] | null, currEdits: TextChange[]): TextChange[] {
if (prevEdits === null || prevEdits.length === 0) {
return currEdits;
}
const compressor = new TextChangeCompressor(prevEdits, currEdits);
return compressor.compress();
}
class TextChangeCompressor {
private _prevEdits: TextChange[];
private _currEdits: TextChange[];
private _result: TextChange[];
private _resultLen: number;
private _prevLen: number;
private _prevDeltaOffset: number;
private _currLen: number;
private _currDeltaOffset: number;
constructor(prevEdits: TextChange[], currEdits: TextChange[]) {
this._prevEdits = prevEdits;
this._currEdits = currEdits;
this._result = [];
this._resultLen = 0;
this._prevLen = this._prevEdits.length;
this._prevDeltaOffset = 0;
this._currLen = this._currEdits.length;
this._currDeltaOffset = 0;
}
public compress(): TextChange[] {
let prevIndex = 0;
let currIndex = 0;
let prevEdit = this._getPrev(prevIndex);
let currEdit = this._getCurr(currIndex);
while (prevIndex < this._prevLen || currIndex < this._currLen) {
if (prevEdit === null) {
this._acceptCurr(currEdit!);
currEdit = this._getCurr(++currIndex);
continue;
}
if (currEdit === null) {
this._acceptPrev(prevEdit);
prevEdit = this._getPrev(++prevIndex);
continue;
}
if (currEdit.oldEnd <= prevEdit.newPosition) {
this._acceptCurr(currEdit);
currEdit = this._getCurr(++currIndex);
continue;
}
if (prevEdit.newEnd <= currEdit.oldPosition) {
this._acceptPrev(prevEdit);
prevEdit = this._getPrev(++prevIndex);
continue;
}
if (currEdit.oldPosition < prevEdit.newPosition) {
const [e1, e2] = TextChangeCompressor._splitCurr(currEdit, prevEdit.newPosition - currEdit.oldPosition);
this._acceptCurr(e1);
currEdit = e2;
continue;
}
if (prevEdit.newPosition < currEdit.oldPosition) {
const [e1, e2] = TextChangeCompressor._splitPrev(prevEdit, currEdit.oldPosition - prevEdit.newPosition);
this._acceptPrev(e1);
prevEdit = e2;
continue;
}
// At this point, currEdit.oldPosition === prevEdit.newPosition
let mergePrev: TextChange;
let mergeCurr: TextChange;
if (currEdit.oldEnd === prevEdit.newEnd) {
mergePrev = prevEdit;
mergeCurr = currEdit;
prevEdit = this._getPrev(++prevIndex);
currEdit = this._getCurr(++currIndex);
} else if (currEdit.oldEnd < prevEdit.newEnd) {
const [e1, e2] = TextChangeCompressor._splitPrev(prevEdit, currEdit.oldLength);
mergePrev = e1;
mergeCurr = currEdit;
prevEdit = e2;
currEdit = this._getCurr(++currIndex);
} else {
const [e1, e2] = TextChangeCompressor._splitCurr(currEdit, prevEdit.newLength);
mergePrev = prevEdit;
mergeCurr = e1;
prevEdit = this._getPrev(++prevIndex);
currEdit = e2;
}
this._result[this._resultLen++] = new TextChange(
mergePrev.oldPosition,
mergePrev.oldText,
mergeCurr.newPosition,
mergeCurr.newText
);
this._prevDeltaOffset += mergePrev.newLength - mergePrev.oldLength;
this._currDeltaOffset += mergeCurr.newLength - mergeCurr.oldLength;
}
const merged = TextChangeCompressor._merge(this._result);
const cleaned = TextChangeCompressor._removeNoOps(merged);
return cleaned;
}
private _acceptCurr(currEdit: TextChange): void {
this._result[this._resultLen++] = TextChangeCompressor._rebaseCurr(this._prevDeltaOffset, currEdit);
this._currDeltaOffset += currEdit.newLength - currEdit.oldLength;
}
private _getCurr(currIndex: number): TextChange | null {
return (currIndex < this._currLen ? this._currEdits[currIndex] : null);
}
private _acceptPrev(prevEdit: TextChange): void {
this._result[this._resultLen++] = TextChangeCompressor._rebasePrev(this._currDeltaOffset, prevEdit);
this._prevDeltaOffset += prevEdit.newLength - prevEdit.oldLength;
}
private _getPrev(prevIndex: number): TextChange | null {
return (prevIndex < this._prevLen ? this._prevEdits[prevIndex] : null);
}
private static _rebaseCurr(prevDeltaOffset: number, currEdit: TextChange): TextChange {
return new TextChange(
currEdit.oldPosition - prevDeltaOffset,
currEdit.oldText,
currEdit.newPosition,
currEdit.newText
);
}
private static _rebasePrev(currDeltaOffset: number, prevEdit: TextChange): TextChange {
return new TextChange(
prevEdit.oldPosition,
prevEdit.oldText,
prevEdit.newPosition + currDeltaOffset,
prevEdit.newText
);
}
private static _splitPrev(edit: TextChange, offset: number): [TextChange, TextChange] {
const preText = edit.newText.substr(0, offset);
const postText = edit.newText.substr(offset);
return [
new TextChange(
edit.oldPosition,
edit.oldText,
edit.newPosition,
preText
),
new TextChange(
edit.oldEnd,
'',
edit.newPosition + offset,
postText
)
];
}
private static _splitCurr(edit: TextChange, offset: number): [TextChange, TextChange] {
const preText = edit.oldText.substr(0, offset);
const postText = edit.oldText.substr(offset);
return [
new TextChange(
edit.oldPosition,
preText,
edit.newPosition,
edit.newText
),
new TextChange(
edit.oldPosition + offset,
postText,
edit.newEnd,
''
)
];
}
private static _merge(edits: TextChange[]): TextChange[] {
if (edits.length === 0) {
return edits;
}
let result: TextChange[] = [], resultLen = 0;
let prev = edits[0];
for (let i = 1; i < edits.length; i++) {
const curr = edits[i];
if (prev.oldEnd === curr.oldPosition) {
// Merge into `prev`
prev = new TextChange(
prev.oldPosition,
prev.oldText + curr.oldText,
prev.newPosition,
prev.newText + curr.newText
);
} else {
result[resultLen++] = prev;
prev = curr;
}
}
result[resultLen++] = prev;
return result;
}
private static _removeNoOps(edits: TextChange[]): TextChange[] {
if (edits.length === 0) {
return edits;
}
let result: TextChange[] = [], resultLen = 0;
for (let i = 0; i < edits.length; i++) {
const edit = edits[i];
if (edit.oldText === edit.newText) {
continue;
}
result[resultLen++] = edit;
}
return result;
}
}

View File

@@ -37,6 +37,7 @@ import { Color } from 'vs/base/common/color';
import { Constants } from 'vs/base/common/uint'; import { Constants } from 'vs/base/common/uint';
import { EditorTheme } from 'vs/editor/common/view/viewContext'; import { EditorTheme } from 'vs/editor/common/view/viewContext';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { TextChange } from 'vs/editor/common/model/textChange';
function createTextBufferBuilder() { function createTextBufferBuilder() {
return new PieceTreeTextBufferBuilder(); return new PieceTreeTextBufferBuilder();
@@ -367,7 +368,6 @@ export class TextModel extends Disposable implements model.ITextModel {
this._onWillDispose.fire(); this._onWillDispose.fire();
this._languageRegistryListener.dispose(); this._languageRegistryListener.dispose();
this._tokenization.dispose(); this._tokenization.dispose();
this._undoRedoService.removeElements(this.uri);
this._isDisposed = true; this._isDisposed = true;
super.dispose(); super.dispose();
this._isDisposing = false; this._isDisposing = false;
@@ -711,7 +711,11 @@ export class TextModel extends Disposable implements model.ITextModel {
this._alternativeVersionId = this._versionId; this._alternativeVersionId = this._versionId;
} }
private _overwriteAlternativeVersionId(newAlternativeVersionId: number): void { public _overwriteVersionId(versionId: number): void {
this._versionId = versionId;
}
public _overwriteAlternativeVersionId(newAlternativeVersionId: number): void {
this._alternativeVersionId = newAlternativeVersionId; this._alternativeVersionId = newAlternativeVersionId;
} }
@@ -1285,19 +1289,39 @@ export class TextModel extends Disposable implements model.ITextModel {
return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer); return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer);
} }
_applyUndoRedoEdits(edits: model.IValidEditOperations[], eol: model.EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): model.IValidEditOperations[] { _applyUndo(changes: TextChange[], eol: model.EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void {
const edits = changes.map<model.IIdentifiedSingleEditOperation>((change) => {
const rangeStart = this.getPositionAt(change.newPosition);
const rangeEnd = this.getPositionAt(change.newEnd);
return {
range: new Range(rangeStart.lineNumber, rangeStart.column, rangeEnd.lineNumber, rangeEnd.column),
text: change.oldText
};
});
this._applyUndoRedoEdits(edits, eol, true, false, resultingAlternativeVersionId, resultingSelection);
}
_applyRedo(changes: TextChange[], eol: model.EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void {
const edits = changes.map<model.IIdentifiedSingleEditOperation>((change) => {
const rangeStart = this.getPositionAt(change.oldPosition);
const rangeEnd = this.getPositionAt(change.oldEnd);
return {
range: new Range(rangeStart.lineNumber, rangeStart.column, rangeEnd.lineNumber, rangeEnd.column),
text: change.newText
};
});
this._applyUndoRedoEdits(edits, eol, false, true, resultingAlternativeVersionId, resultingSelection);
}
private _applyUndoRedoEdits(edits: model.IIdentifiedSingleEditOperation[], eol: model.EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void {
try { try {
this._onDidChangeDecorations.beginDeferredEmit(); this._onDidChangeDecorations.beginDeferredEmit();
this._eventEmitter.beginDeferredEmit(); this._eventEmitter.beginDeferredEmit();
this._isUndoing = isUndoing; this._isUndoing = isUndoing;
this._isRedoing = isRedoing; this._isRedoing = isRedoing;
let reverseEdits: model.IValidEditOperations[] = []; this.applyEdits(edits, false);
for (let i = 0, len = edits.length; i < len; i++) {
reverseEdits[i] = { operations: this.applyEdits(edits[i].operations) };
}
this.setEOL(eol); this.setEOL(eol);
this._overwriteAlternativeVersionId(resultingAlternativeVersionId); this._overwriteAlternativeVersionId(resultingAlternativeVersionId);
return reverseEdits;
} finally { } finally {
this._isUndoing = false; this._isUndoing = false;
this._isRedoing = false; this._isRedoing = false;
@@ -1306,21 +1330,25 @@ export class TextModel extends Disposable implements model.ITextModel {
} }
} }
public applyEdits(rawOperations: model.IIdentifiedSingleEditOperation[]): model.IValidEditOperation[] { public applyEdits(operations: model.IIdentifiedSingleEditOperation[]): void;
public applyEdits(operations: model.IIdentifiedSingleEditOperation[], computeUndoEdits: false): void;
public applyEdits(operations: model.IIdentifiedSingleEditOperation[], computeUndoEdits: true): model.IValidEditOperation[];
public applyEdits(rawOperations: model.IIdentifiedSingleEditOperation[], computeUndoEdits: boolean = false): void | model.IValidEditOperation[] {
try { try {
this._onDidChangeDecorations.beginDeferredEmit(); this._onDidChangeDecorations.beginDeferredEmit();
this._eventEmitter.beginDeferredEmit(); this._eventEmitter.beginDeferredEmit();
return this._doApplyEdits(this._validateEditOperations(rawOperations)); const operations = this._validateEditOperations(rawOperations);
return this._doApplyEdits(operations, computeUndoEdits);
} finally { } finally {
this._eventEmitter.endDeferredEmit(); this._eventEmitter.endDeferredEmit();
this._onDidChangeDecorations.endDeferredEmit(); this._onDidChangeDecorations.endDeferredEmit();
} }
} }
private _doApplyEdits(rawOperations: model.ValidAnnotatedEditOperation[]): model.IValidEditOperation[] { private _doApplyEdits(rawOperations: model.ValidAnnotatedEditOperation[], computeUndoEdits: boolean): void | model.IValidEditOperation[] {
const oldLineCount = this._buffer.getLineCount(); const oldLineCount = this._buffer.getLineCount();
const result = this._buffer.applyEdits(rawOperations, this._options.trimAutoWhitespace); const result = this._buffer.applyEdits(rawOperations, this._options.trimAutoWhitespace, computeUndoEdits);
const newLineCount = this._buffer.getLineCount(); const newLineCount = this._buffer.getLineCount();
const contentChanges = result.changes; const contentChanges = result.changes;
@@ -1395,7 +1423,7 @@ export class TextModel extends Disposable implements model.ITextModel {
); );
} }
return result.reverseEdits; return (result.reverseEdits === null ? undefined : result.reverseEdits);
} }
public undo(): void { public undo(): void {

View File

@@ -11,10 +11,18 @@ import { ColorId, FontStyle, LanguageId, MetadataConsts, StandardTokenType, Toke
import { writeUInt32BE, readUInt32BE } from 'vs/base/common/buffer'; import { writeUInt32BE, readUInt32BE } from 'vs/base/common/buffer';
import { CharCode } from 'vs/base/common/charCode'; import { CharCode } from 'vs/base/common/charCode';
export function countEOL(text: string): [number, number, number] { export const enum StringEOL {
Unknown = 0,
Invalid = 3,
LF = 1,
CRLF = 2
}
export function countEOL(text: string): [number, number, number, StringEOL] {
let eolCount = 0; let eolCount = 0;
let firstLineLength = 0; let firstLineLength = 0;
let lastLineStart = 0; let lastLineStart = 0;
let eol: StringEOL = StringEOL.Unknown;
for (let i = 0, len = text.length; i < len; i++) { for (let i = 0, len = text.length; i < len; i++) {
const chr = text.charCodeAt(i); const chr = text.charCodeAt(i);
@@ -25,12 +33,16 @@ export function countEOL(text: string): [number, number, number] {
eolCount++; eolCount++;
if (i + 1 < len && text.charCodeAt(i + 1) === CharCode.LineFeed) { if (i + 1 < len && text.charCodeAt(i + 1) === CharCode.LineFeed) {
// \r\n... case // \r\n... case
eol |= StringEOL.CRLF;
i++; // skip \n i++; // skip \n
} else { } else {
// \r... case // \r... case
eol |= StringEOL.Invalid;
} }
lastLineStart = i + 1; lastLineStart = i + 1;
} else if (chr === CharCode.LineFeed) { } else if (chr === CharCode.LineFeed) {
// \n... case
eol |= StringEOL.LF;
if (eolCount === 0) { if (eolCount === 0) {
firstLineLength = i; firstLineLength = i;
} }
@@ -41,7 +53,7 @@ export function countEOL(text: string): [number, number, number] {
if (eolCount === 0) { if (eolCount === 0) {
firstLineLength = text.length; firstLineLength = text.length;
} }
return [eolCount, firstLineLength, text.length - lastLineStart]; return [eolCount, firstLineLength, text.length - lastLineStart, eol];
} }
function getDefaultMetadata(topLevelLanguageId: LanguageId): number { function getDefaultMetadata(topLevelLanguageId: LanguageId): number {

View File

@@ -1396,6 +1396,15 @@ export interface AuthenticationSession {
accountName: string; accountName: string;
} }
/**
* @internal
*/
export interface AuthenticationSessionsChangeEvent {
added: string[];
removed: string[];
changed: string[];
}
export interface Command { export interface Command {
id: string; id: string;
title: string; title: string;

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { Emitter, Event } from 'vs/base/common/event'; import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { Disposable, IDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle';
import * as platform from 'vs/base/common/platform'; import * as platform from 'vs/base/common/platform';
@@ -25,7 +26,14 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { SparseEncodedTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore'; import { SparseEncodedTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore';
import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { IUndoRedoService, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo';
import { StringSHA1 } from 'vs/base/common/hash';
import { SingleModelEditStackElement, MultiModelEditStackElement, EditStackElement } from 'vs/editor/common/model/editStack';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { Schemas } from 'vs/base/common/network';
import Severity from 'vs/base/common/severity';
export const MAINTAIN_UNDO_REDO_STACK = true;
export interface IEditorSemanticHighlightingOptions { export interface IEditorSemanticHighlightingOptions {
enabled?: boolean; enabled?: boolean;
@@ -35,6 +43,18 @@ function MODEL_ID(resource: URI): string {
return resource.toString(); return resource.toString();
} }
function computeModelSha1(model: ITextModel): string {
// compute the sha1
const shaComputer = new StringSHA1();
const snapshot = model.createSnapshot();
let text: string | null;
while ((text = snapshot.read())) {
shaComputer.update(text);
}
return shaComputer.digest();
}
class ModelData implements IDisposable { class ModelData implements IDisposable {
public readonly model: ITextModel; public readonly model: ITextModel;
@@ -98,13 +118,42 @@ interface IRawConfig {
const DEFAULT_EOL = (platform.isLinux || platform.isMacintosh) ? DefaultEndOfLine.LF : DefaultEndOfLine.CRLF; const DEFAULT_EOL = (platform.isLinux || platform.isMacintosh) ? DefaultEndOfLine.LF : DefaultEndOfLine.CRLF;
export class ModelServiceImpl extends Disposable implements IModelService { interface EditStackPastFutureElements {
public _serviceBrand: undefined; past: EditStackElement[];
future: EditStackElement[];
}
private readonly _configurationService: IConfigurationService; function isEditStackPastFutureElements(undoElements: IPastFutureElements): undoElements is EditStackPastFutureElements {
private readonly _configurationServiceSubscription: IDisposable; return (isEditStackElements(undoElements.past) && isEditStackElements(undoElements.future));
private readonly _resourcePropertiesService: ITextResourcePropertiesService; }
private readonly _undoRedoService: IUndoRedoService;
function isEditStackElements(elements: IUndoRedoElement[]): elements is EditStackElement[] {
for (const element of elements) {
if (element instanceof SingleModelEditStackElement) {
continue;
}
if (element instanceof MultiModelEditStackElement) {
continue;
}
return false;
}
return true;
}
class DisposedModelInfo {
constructor(
public readonly uri: URI,
public readonly sha1: string,
public readonly versionId: number,
public readonly alternativeVersionId: number,
) { }
}
export class ModelServiceImpl extends Disposable implements IModelService {
private static _PROMPT_UNDO_REDO_SIZE_LIMIT = 10 * 1024 * 1024; // 10MB
public _serviceBrand: undefined;
private readonly _onModelAdded: Emitter<ITextModel> = this._register(new Emitter<ITextModel>()); private readonly _onModelAdded: Emitter<ITextModel> = this._register(new Emitter<ITextModel>());
public readonly onModelAdded: Event<ITextModel> = this._onModelAdded.event; public readonly onModelAdded: Event<ITextModel> = this._onModelAdded.event;
@@ -115,39 +164,37 @@ export class ModelServiceImpl extends Disposable implements IModelService {
private readonly _onModelModeChanged: Emitter<{ model: ITextModel; oldModeId: string; }> = this._register(new Emitter<{ model: ITextModel; oldModeId: string; }>()); private readonly _onModelModeChanged: Emitter<{ model: ITextModel; oldModeId: string; }> = this._register(new Emitter<{ model: ITextModel; oldModeId: string; }>());
public readonly onModelModeChanged: Event<{ model: ITextModel; oldModeId: string; }> = this._onModelModeChanged.event; public readonly onModelModeChanged: Event<{ model: ITextModel; oldModeId: string; }> = this._onModelModeChanged.event;
private _modelCreationOptionsByLanguageAndResource: { private _modelCreationOptionsByLanguageAndResource: { [languageAndResource: string]: ITextModelCreationOptions; };
[languageAndResource: string]: ITextModelCreationOptions;
};
/** /**
* All the models known in the system. * All the models known in the system.
*/ */
private readonly _models: { [modelId: string]: ModelData; }; private readonly _models: { [modelId: string]: ModelData; };
private readonly _disposedModels: Map<string, DisposedModelInfo>;
constructor( constructor(
@IConfigurationService configurationService: IConfigurationService, @IConfigurationService private readonly _configurationService: IConfigurationService,
@ITextResourcePropertiesService resourcePropertiesService: ITextResourcePropertiesService, @ITextResourcePropertiesService private readonly _resourcePropertiesService: ITextResourcePropertiesService,
@IThemeService themeService: IThemeService, @IThemeService private readonly _themeService: IThemeService,
@ILogService logService: ILogService, @ILogService private readonly _logService: ILogService,
@IUndoRedoService undoRedoService: IUndoRedoService @IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
@IDialogService private readonly _dialogService: IDialogService,
) { ) {
super(); super();
this._configurationService = configurationService;
this._resourcePropertiesService = resourcePropertiesService;
this._undoRedoService = undoRedoService;
this._models = {};
this._modelCreationOptionsByLanguageAndResource = Object.create(null); this._modelCreationOptionsByLanguageAndResource = Object.create(null);
this._models = {};
this._disposedModels = new Map<string, DisposedModelInfo>();
this._configurationServiceSubscription = this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions()); this._register(this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions()));
this._updateModelOptions(); this._updateModelOptions();
this._register(new SemanticColoringFeature(this, themeService, configurationService, logService)); this._register(new SemanticColoringFeature(this, this._themeService, this._configurationService, this._logService));
} }
private static _readModelOptions(config: IRawConfig, isForSimpleWidget: boolean): ITextModelCreationOptions { private static _readModelOptions(config: IRawConfig, isForSimpleWidget: boolean): ITextModelCreationOptions {
let tabSize = EDITOR_MODEL_DEFAULTS.tabSize; let tabSize = EDITOR_MODEL_DEFAULTS.tabSize;
if (config.editor && typeof config.editor.tabSize !== 'undefined') { if (config.editor && typeof config.editor.tabSize !== 'undefined') {
let parsedTabSize = parseInt(config.editor.tabSize, 10); const parsedTabSize = parseInt(config.editor.tabSize, 10);
if (!isNaN(parsedTabSize)) { if (!isNaN(parsedTabSize)) {
tabSize = parsedTabSize; tabSize = parsedTabSize;
} }
@@ -158,7 +205,7 @@ export class ModelServiceImpl extends Disposable implements IModelService {
let indentSize = tabSize; let indentSize = tabSize;
if (config.editor && typeof config.editor.indentSize !== 'undefined' && config.editor.indentSize !== 'tabSize') { if (config.editor && typeof config.editor.indentSize !== 'undefined' && config.editor.indentSize !== 'tabSize') {
let parsedIndentSize = parseInt(config.editor.indentSize, 10); const parsedIndentSize = parseInt(config.editor.indentSize, 10);
if (!isNaN(parsedIndentSize)) { if (!isNaN(parsedIndentSize)) {
indentSize = parsedIndentSize; indentSize = parsedIndentSize;
} }
@@ -230,14 +277,14 @@ export class ModelServiceImpl extends Disposable implements IModelService {
} }
private _updateModelOptions(): void { private _updateModelOptions(): void {
let oldOptionsByLanguageAndResource = this._modelCreationOptionsByLanguageAndResource; const oldOptionsByLanguageAndResource = this._modelCreationOptionsByLanguageAndResource;
this._modelCreationOptionsByLanguageAndResource = Object.create(null); this._modelCreationOptionsByLanguageAndResource = Object.create(null);
// Update options on all models // Update options on all models
let keys = Object.keys(this._models); const keys = Object.keys(this._models);
for (let i = 0, len = keys.length; i < len; i++) { for (let i = 0, len = keys.length; i < len; i++) {
let modelId = keys[i]; const modelId = keys[i];
let modelData = this._models[modelId]; const modelData = this._models[modelId];
const language = modelData.model.getLanguageIdentifier().language; const language = modelData.model.getLanguageIdentifier().language;
const uri = modelData.model.uri; const uri = modelData.model.uri;
const oldOptions = oldOptionsByLanguageAndResource[language + uri]; const oldOptions = oldOptionsByLanguageAndResource[language + uri];
@@ -277,17 +324,30 @@ export class ModelServiceImpl extends Disposable implements IModelService {
} }
} }
public dispose(): void {
this._configurationServiceSubscription.dispose();
super.dispose();
}
// --- begin IModelService // --- begin IModelService
private _createModelData(value: string | ITextBufferFactory, languageIdentifier: LanguageIdentifier, resource: URI | undefined, isForSimpleWidget: boolean): ModelData { private _createModelData(value: string | ITextBufferFactory, languageIdentifier: LanguageIdentifier, resource: URI | undefined, isForSimpleWidget: boolean): ModelData {
// create & save the model // create & save the model
const options = this.getCreationOptions(languageIdentifier.language, resource, isForSimpleWidget); const options = this.getCreationOptions(languageIdentifier.language, resource, isForSimpleWidget);
const model: TextModel = new TextModel(value, options, languageIdentifier, resource, this._undoRedoService); const model: TextModel = new TextModel(value, options, languageIdentifier, resource, this._undoRedoService);
if (resource && this._disposedModels.has(MODEL_ID(resource))) {
const disposedModelData = this._disposedModels.get(MODEL_ID(resource))!;
this._disposedModels.delete(MODEL_ID(resource));
const elements = this._undoRedoService.getElements(resource);
if (computeModelSha1(model) === disposedModelData.sha1 && isEditStackPastFutureElements(elements)) {
for (const element of elements.past) {
element.setModel(model);
}
for (const element of elements.future) {
element.setModel(model);
}
this._undoRedoService.setElementsIsValid(resource, true);
model._overwriteVersionId(disposedModelData.versionId);
model._overwriteAlternativeVersionId(disposedModelData.alternativeVersionId);
} else {
this._undoRedoService.removeElements(resource);
}
}
const modelId = MODEL_ID(model.uri); const modelId = MODEL_ID(model.uri);
if (this._models[modelId]) { if (this._models[modelId]) {
@@ -360,7 +420,8 @@ export class ModelServiceImpl extends Disposable implements IModelService {
const commonSuffix = this._commonSuffix(model, modelLineCount - commonPrefix, commonPrefix, textBuffer, textBufferLineCount - commonPrefix, commonPrefix); const commonSuffix = this._commonSuffix(model, modelLineCount - commonPrefix, commonPrefix, textBuffer, textBufferLineCount - commonPrefix, commonPrefix);
let oldRange: Range, newRange: Range; let oldRange: Range;
let newRange: Range;
if (commonSuffix > 0) { if (commonSuffix > 0) {
oldRange = new Range(commonPrefix + 1, 1, modelLineCount - commonSuffix + 1, 1); oldRange = new Range(commonPrefix + 1, 1, modelLineCount - commonSuffix + 1, 1);
newRange = new Range(commonPrefix + 1, 1, textBufferLineCount - commonSuffix + 1, 1); newRange = new Range(commonPrefix + 1, 1, textBufferLineCount - commonSuffix + 1, 1);
@@ -394,7 +455,7 @@ export class ModelServiceImpl extends Disposable implements IModelService {
if (!languageSelection) { if (!languageSelection) {
return; return;
} }
let modelData = this._models[MODEL_ID(model.uri)]; const modelData = this._models[MODEL_ID(model.uri)];
if (!modelData) { if (!modelData) {
return; return;
} }
@@ -403,19 +464,69 @@ export class ModelServiceImpl extends Disposable implements IModelService {
public destroyModel(resource: URI): void { public destroyModel(resource: URI): void {
// We need to support that not all models get disposed through this service (i.e. model.dispose() should work!) // We need to support that not all models get disposed through this service (i.e. model.dispose() should work!)
let modelData = this._models[MODEL_ID(resource)]; const modelData = this._models[MODEL_ID(resource)];
if (!modelData) { if (!modelData) {
return; return;
} }
const model = modelData.model;
let maintainUndoRedoStack = false;
let heapSize = 0;
if (MAINTAIN_UNDO_REDO_STACK && (resource.scheme === Schemas.file || resource.scheme === Schemas.vscodeRemote)) {
const elements = this._undoRedoService.getElements(resource);
if ((elements.past.length > 0 || elements.future.length > 0) && isEditStackPastFutureElements(elements)) {
maintainUndoRedoStack = true;
for (const element of elements.past) {
heapSize += element.heapSize(resource);
element.setModel(resource); // remove reference from text buffer instance
}
for (const element of elements.future) {
heapSize += element.heapSize(resource);
element.setModel(resource); // remove reference from text buffer instance
}
} else {
maintainUndoRedoStack = false;
}
}
if (maintainUndoRedoStack) {
// We only invalidate the elements, but they remain in the undo-redo service.
this._undoRedoService.setElementsIsValid(resource, false);
this._disposedModels.set(MODEL_ID(resource), new DisposedModelInfo(resource, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId()));
} else {
this._undoRedoService.removeElements(resource);
}
modelData.model.dispose(); modelData.model.dispose();
// After disposing the model, prompt and ask if we should keep the undo-redo stack
if (maintainUndoRedoStack && heapSize > ModelServiceImpl._PROMPT_UNDO_REDO_SIZE_LIMIT) {
const mbSize = (heapSize / 1024 / 1024).toFixed(1);
this._dialogService.show(
Severity.Info,
nls.localize('undoRedoConfirm', "Keep the undo-redo stack for {0} in memory ({1} MB)?", (resource.scheme === Schemas.file ? resource.fsPath : resource.path), mbSize),
[
nls.localize('nok', "Discard"),
nls.localize('ok', "Keep"),
],
{
cancelId: 2
}
).then((result) => {
const discard = (result.choice === 2 || result.choice === 0);
if (discard) {
this._disposedModels.delete(MODEL_ID(resource));
this._undoRedoService.removeElements(resource);
}
});
}
} }
public getModels(): ITextModel[] { public getModels(): ITextModel[] {
let ret: ITextModel[] = []; const ret: ITextModel[] = [];
let keys = Object.keys(this._models); const keys = Object.keys(this._models);
for (let i = 0, len = keys.length; i < len; i++) { for (let i = 0, len = keys.length; i < len; i++) {
let modelId = keys[i]; const modelId = keys[i];
ret.push(this._models[modelId].model); ret.push(this._models[modelId].model);
} }
@@ -423,8 +534,8 @@ export class ModelServiceImpl extends Disposable implements IModelService {
} }
public getModel(resource: URI): ITextModel | null { public getModel(resource: URI): ITextModel | null {
let modelId = MODEL_ID(resource); const modelId = MODEL_ID(resource);
let modelData = this._models[modelId]; const modelData = this._models[modelId];
if (!modelData) { if (!modelData) {
return null; return null;
} }
@@ -434,8 +545,8 @@ export class ModelServiceImpl extends Disposable implements IModelService {
// --- end IModelService // --- end IModelService
private _onWillDispose(model: ITextModel): void { private _onWillDispose(model: ITextModel): void {
let modelId = MODEL_ID(model.uri); const modelId = MODEL_ID(model.uri);
let modelData = this._models[modelId]; const modelData = this._models[modelId];
delete this._models[modelId]; delete this._models[modelId];
modelData.dispose(); modelData.dispose();

View File

@@ -36,7 +36,7 @@ export namespace InspectTokensNLS {
} }
export namespace GoToLineNLS { export namespace GoToLineNLS {
export const gotoLineActionLabel = nls.localize('gotoLineActionLabel', "Go to Line..."); export const gotoLineActionLabel = nls.localize('gotoLineActionLabel', "Go to Line/Column...");
} }
export namespace QuickHelpNLS { export namespace QuickHelpNLS {

View File

@@ -22,6 +22,10 @@ interface IEditorLineDecoration {
overviewRulerDecorationId: string; overviewRulerDecorationId: string;
} }
export interface IEditorNavigationQuickAccessOptions {
canAcceptInBackground?: boolean;
}
/** /**
* A reusable quick access provider for the editor with support * A reusable quick access provider for the editor with support
* for adding decorations for navigating in the currently active file * for adding decorations for navigating in the currently active file
@@ -29,11 +33,16 @@ interface IEditorLineDecoration {
*/ */
export abstract class AbstractEditorNavigationQuickAccessProvider implements IQuickAccessProvider { export abstract class AbstractEditorNavigationQuickAccessProvider implements IQuickAccessProvider {
constructor(protected options?: IEditorNavigationQuickAccessOptions) { }
//#region Provider methods //#region Provider methods
provide(picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable { provide(picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable {
const disposables = new DisposableStore(); const disposables = new DisposableStore();
// Apply options if any
picker.canAcceptInBackground = !!this.options?.canAcceptInBackground;
// Disable filtering & sorting, we control the results // Disable filtering & sorting, we control the results
picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false; picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false;
@@ -71,11 +80,11 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu
lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState()); lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState());
})); }));
once(token.onCancellationRequested)(() => { disposables.add(once(token.onCancellationRequested)(() => {
if (lastKnownEditorViewState) { if (lastKnownEditorViewState) {
editor.restoreViewState(lastKnownEditorViewState); editor.restoreViewState(lastKnownEditorViewState);
} }
}); }));
} }
// Clean up decorations on dispose // Clean up decorations on dispose
@@ -110,10 +119,12 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu
*/ */
protected abstract provideWithoutTextEditor(picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable; protected abstract provideWithoutTextEditor(picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable;
protected gotoLocation(editor: IEditor, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean }): void { protected gotoLocation(editor: IEditor, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean, preserveFocus?: boolean }): void {
editor.setSelection(options.range); editor.setSelection(options.range);
editor.revealRangeInCenter(options.range, ScrollType.Smooth); editor.revealRangeInCenter(options.range, ScrollType.Smooth);
editor.focus(); if (!options.preserveFocus) {
editor.focus();
}
} }
protected getModel(editor: IEditor | IDiffEditor): ITextModel | undefined { protected getModel(editor: IEditor | IDiffEditor): ITextModel | undefined {

View File

@@ -6,11 +6,13 @@
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { DisposableStore, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { DisposableStore, IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IEditor, ScrollType } from 'vs/editor/common/editorCommon'; import { IEditor, ScrollType } from 'vs/editor/common/editorCommon';
import { IRange } from 'vs/editor/common/core/range'; import { IRange } from 'vs/editor/common/core/range';
import { AbstractEditorNavigationQuickAccessProvider } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; import { AbstractEditorNavigationQuickAccessProvider } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess';
import { IPosition } from 'vs/editor/common/core/position'; import { IPosition } from 'vs/editor/common/core/position';
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions';
interface IGotoLineQuickPickItem extends IQuickPickItem, Partial<IPosition> { } interface IGotoLineQuickPickItem extends IQuickPickItem, Partial<IPosition> { }
@@ -18,6 +20,10 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
static PREFIX = ':'; static PREFIX = ':';
constructor() {
super({ canAcceptInBackground: true });
}
protected provideWithoutTextEditor(picker: IQuickPick<IGotoLineQuickPickItem>): IDisposable { protected provideWithoutTextEditor(picker: IQuickPick<IGotoLineQuickPickItem>): IDisposable {
const label = localize('cannotRunGotoLine', "Open a text editor first to go to a line."); const label = localize('cannotRunGotoLine', "Open a text editor first to go to a line.");
@@ -31,16 +37,18 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
const disposables = new DisposableStore(); const disposables = new DisposableStore();
// Goto line once picked // Goto line once picked
disposables.add(picker.onDidAccept(() => { disposables.add(picker.onDidAccept(event => {
const [item] = picker.selectedItems; const [item] = picker.selectedItems;
if (item) { if (item) {
if (!this.isValidLineNumber(editor, item.lineNumber)) { if (!this.isValidLineNumber(editor, item.lineNumber)) {
return; return;
} }
this.gotoLocation(editor, { range: this.toRange(item.lineNumber, item.column), keyMods: picker.keyMods }); this.gotoLocation(editor, { range: this.toRange(item.lineNumber, item.column), keyMods: picker.keyMods, preserveFocus: event.inBackground });
picker.hide(); if (!event.inBackground) {
picker.hide();
}
} }
})); }));
@@ -75,6 +83,18 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
updatePickerAndEditor(); updatePickerAndEditor();
disposables.add(picker.onDidChangeValue(() => updatePickerAndEditor())); disposables.add(picker.onDidChangeValue(() => updatePickerAndEditor()));
// Adjust line number visibility as needed
const codeEditor = getCodeEditor(editor);
if (codeEditor) {
const options = codeEditor.getOptions();
const lineNumbers = options.get(EditorOption.lineNumbers);
if (lineNumbers.renderType === RenderLineNumbersType.Relative) {
codeEditor.updateOptions({ lineNumbers: 'on' });
disposables.add(toDisposable(() => codeEditor.updateOptions({ lineNumbers: 'relative' })));
}
}
return disposables; return disposables;
} }

View File

@@ -6,25 +6,26 @@
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { IQuickPick, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IQuickPick, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { DisposableStore, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { DisposableStore, IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IEditor, ScrollType } from 'vs/editor/common/editorCommon'; import { IEditor, ScrollType } from 'vs/editor/common/editorCommon';
import { ITextModel } from 'vs/editor/common/model'; import { ITextModel } from 'vs/editor/common/model';
import { IRange, Range } from 'vs/editor/common/core/range'; import { IRange, Range } from 'vs/editor/common/core/range';
import { AbstractEditorNavigationQuickAccessProvider } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; import { AbstractEditorNavigationQuickAccessProvider, IEditorNavigationQuickAccessOptions } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess';
import { DocumentSymbol, SymbolKinds, SymbolTag, DocumentSymbolProviderRegistry, SymbolKind } from 'vs/editor/common/modes'; import { DocumentSymbol, SymbolKinds, SymbolTag, DocumentSymbolProviderRegistry, SymbolKind } from 'vs/editor/common/modes';
import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { values } from 'vs/base/common/collections'; import { values } from 'vs/base/common/collections';
import { trim, format } from 'vs/base/common/strings'; import { trim, format } from 'vs/base/common/strings';
import { fuzzyScore, FuzzyScore, createMatches } from 'vs/base/common/filters'; import { fuzzyScore, FuzzyScore, createMatches } from 'vs/base/common/filters';
import { assign } from 'vs/base/common/objects';
interface IGotoSymbolQuickPickItem extends IQuickPickItem { export interface IGotoSymbolQuickPickItem extends IQuickPickItem {
kind: SymbolKind, kind: SymbolKind,
index: number, index: number,
score?: FuzzyScore; score?: FuzzyScore;
range?: { decoration: IRange, selection: IRange }, range?: { decoration: IRange, selection: IRange }
} }
export interface IGotoSymbolQuickAccessProviderOptions { export interface IGotoSymbolQuickAccessProviderOptions extends IEditorNavigationQuickAccessOptions {
openSideBySideDirection: () => undefined | 'right' | 'down' openSideBySideDirection: () => undefined | 'right' | 'down'
} }
@@ -34,8 +35,8 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
static SCOPE_PREFIX = ':'; static SCOPE_PREFIX = ':';
static PREFIX_BY_CATEGORY = `${AbstractGotoSymbolQuickAccessProvider.PREFIX}${AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX}`; static PREFIX_BY_CATEGORY = `${AbstractGotoSymbolQuickAccessProvider.PREFIX}${AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX}`;
constructor(private options?: IGotoSymbolQuickAccessProviderOptions) { constructor(protected options?: IGotoSymbolQuickAccessProviderOptions) {
super(); super(assign(options, { canAcceptInBackground: true }));
} }
protected provideWithoutTextEditor(picker: IQuickPick<IGotoSymbolQuickPickItem>): IDisposable { protected provideWithoutTextEditor(picker: IQuickPick<IGotoSymbolQuickPickItem>): IDisposable {
@@ -72,32 +73,58 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
picker.items = [{ label, index: 0, kind: SymbolKind.String }]; picker.items = [{ label, index: 0, kind: SymbolKind.String }];
picker.ariaLabel = label; picker.ariaLabel = label;
// Listen to changes to the registry and see if eventually // Wait for changes to the registry and see if eventually
// we do get symbols. This can happen if the picker is opened // we do get symbols. This can happen if the picker is opened
// very early after the model has loaded but before the // very early after the model has loaded but before the
// language registry is ready. // language registry is ready.
// https://github.com/microsoft/vscode/issues/70607 // https://github.com/microsoft/vscode/issues/70607
(async () => {
const result = await this.waitForLanguageSymbolRegistry(model, disposables);
if (!result || token.isCancellationRequested) {
return;
}
disposables.add(this.doProvideWithEditorSymbols(editor, model, picker, token));
})();
return disposables;
}
protected async waitForLanguageSymbolRegistry(model: ITextModel, disposables: DisposableStore): Promise<boolean> {
if (DocumentSymbolProviderRegistry.has(model)) {
return true;
}
let symbolProviderRegistryPromiseResolve: (res: boolean) => void;
const symbolProviderRegistryPromise = new Promise<boolean>(resolve => symbolProviderRegistryPromiseResolve = resolve);
// Resolve promise when registry knows model
const symbolProviderListener = disposables.add(DocumentSymbolProviderRegistry.onDidChange(() => { const symbolProviderListener = disposables.add(DocumentSymbolProviderRegistry.onDidChange(() => {
if (DocumentSymbolProviderRegistry.has(model)) { if (DocumentSymbolProviderRegistry.has(model)) {
symbolProviderListener.dispose(); symbolProviderListener.dispose();
disposables.add(this.doProvideWithEditorSymbols(editor, model, picker, token)); symbolProviderRegistryPromiseResolve(true);
} }
})); }));
return disposables; // Resolve promise when we get disposed too
disposables.add(toDisposable(() => symbolProviderRegistryPromiseResolve(false)));
return symbolProviderRegistryPromise;
} }
private doProvideWithEditorSymbols(editor: IEditor, model: ITextModel, picker: IQuickPick<IGotoSymbolQuickPickItem>, token: CancellationToken): IDisposable { private doProvideWithEditorSymbols(editor: IEditor, model: ITextModel, picker: IQuickPick<IGotoSymbolQuickPickItem>, token: CancellationToken): IDisposable {
const disposables = new DisposableStore(); const disposables = new DisposableStore();
// Goto symbol once picked // Goto symbol once picked
disposables.add(picker.onDidAccept(() => { disposables.add(picker.onDidAccept(event => {
const [item] = picker.selectedItems; const [item] = picker.selectedItems;
if (item && item.range) { if (item && item.range) {
this.gotoLocation(editor, { range: item.range.selection, keyMods: picker.keyMods }); this.gotoLocation(editor, { range: item.range.selection, keyMods: picker.keyMods, preserveFocus: event.inBackground });
picker.hide(); if (!event.inBackground) {
picker.hide();
}
} }
})); }));
@@ -128,7 +155,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
// Collect symbol picks // Collect symbol picks
picker.busy = true; picker.busy = true;
try { try {
const items = await this.getSymbolPicks(symbolsPromise, picker.value.substr(AbstractGotoSymbolQuickAccessProvider.PREFIX.length).trim(), picksCts.token); const items = await this.doGetSymbolPicks(symbolsPromise, picker.value.substr(AbstractGotoSymbolQuickAccessProvider.PREFIX.length).trim(), picksCts.token);
if (token.isCancellationRequested) { if (token.isCancellationRequested) {
return; return;
} }
@@ -167,7 +194,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
return disposables; return disposables;
} }
private async getSymbolPicks(symbolsPromise: Promise<DocumentSymbol[]>, filter: string, token: CancellationToken): Promise<Array<IGotoSymbolQuickPickItem | IQuickPickSeparator>> { protected async doGetSymbolPicks(symbolsPromise: Promise<DocumentSymbol[]>, filter: string, token: CancellationToken): Promise<Array<IGotoSymbolQuickPickItem | IQuickPickSeparator>> {
const symbols = await symbolsPromise; const symbols = await symbolsPromise;
if (token.isCancellationRequested) { if (token.isCancellationRequested) {
return []; return [];
@@ -340,7 +367,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
return result; return result;
} }
private async getDocumentSymbols(document: ITextModel, flatten: boolean, token: CancellationToken): Promise<DocumentSymbol[]> { protected async getDocumentSymbols(document: ITextModel, flatten: boolean, token: CancellationToken): Promise<DocumentSymbol[]> {
const model = await OutlineModel.create(document, token); const model = await OutlineModel.create(document, token);
if (token.isCancellationRequested) { if (token.isCancellationRequested) {
return []; return [];

View File

@@ -50,7 +50,8 @@ suite('SmartSelect', () => {
setup(() => { setup(() => {
const configurationService = new TestConfigurationService(); const configurationService = new TestConfigurationService();
modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService), new TestThemeService(), new NullLogService(), new UndoRedoService(new TestDialogService(), new TestNotificationService())); const dialogService = new TestDialogService();
modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService()), dialogService);
mode = new MockJSMode(); mode = new MockJSMode();
}); });

View File

@@ -604,6 +604,12 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<Compl
useShadows: false, useShadows: false,
openController: { shouldOpen: () => false }, openController: { shouldOpen: () => false },
mouseSupport: false, mouseSupport: false,
ariaRole: 'listbox',
ariaProvider: {
getRole: () => 'option',
getSetSize: (_: CompletionItem, _index: number, listLength: number) => listLength,
getPosInSet: (_: CompletionItem, index: number) => index,
},
accessibilityProvider: { accessibilityProvider: {
getAriaLabel: (item: CompletionItem) => { getAriaLabel: (item: CompletionItem) => {
const textLabel = typeof item.completion.label === 'string' ? item.completion.label : item.completion.label.name; const textLabel = typeof item.completion.label === 'string' ? item.completion.label : item.completion.label.name;

View File

@@ -41,7 +41,7 @@ export class GotoLineAction extends EditorAction {
super({ super({
id: 'editor.action.gotoLine', id: 'editor.action.gotoLine',
label: GoToLineNLS.gotoLineActionLabel, label: GoToLineNLS.gotoLineActionLabel,
alias: 'Go to Line...', alias: 'Go to Line/Column...',
precondition: undefined, precondition: undefined,
kbOpts: { kbOpts: {
kbExpr: EditorContextKeys.focus, kbExpr: EditorContextKeys.focus,

View File

@@ -156,7 +156,7 @@ export module StaticServices {
export const undoRedoService = define(IUndoRedoService, (o) => new UndoRedoService(dialogService.get(o), notificationService.get(o))); export const undoRedoService = define(IUndoRedoService, (o) => new UndoRedoService(dialogService.get(o), notificationService.get(o)));
export const modelService = define(IModelService, (o) => new ModelServiceImpl(configurationService.get(o), resourcePropertiesService.get(o), standaloneThemeService.get(o), logService.get(o), undoRedoService.get(o))); export const modelService = define(IModelService, (o) => new ModelServiceImpl(configurationService.get(o), resourcePropertiesService.get(o), standaloneThemeService.get(o), logService.get(o), undoRedoService.get(o), dialogService.get(o)));
export const markerDecorationsService = define(IMarkerDecorationsService, (o) => new MarkerDecorationsService(modelService.get(o), markerService.get(o))); export const markerDecorationsService = define(IMarkerDecorationsService, (o) => new MarkerDecorationsService(modelService.get(o), markerService.get(o)));

View File

@@ -1348,7 +1348,7 @@ suite('Editor Controller - Regression tests', () => {
CoreEditingCommands.Undo.runEditorCommand(null, editor, null); CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
assert.equal(model.getLineContent(1), 'Hello world '); assert.equal(model.getLineContent(1), 'Hello world ');
assertCursor(cursor, new Position(1, 13)); assertCursor(cursor, new Selection(1, 12, 1, 13));
CoreEditingCommands.Undo.runEditorCommand(null, editor, null); CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
assert.equal(model.getLineContent(1), 'Hello world'); assert.equal(model.getLineContent(1), 'Hello world');

View File

@@ -54,7 +54,7 @@ for (let fileSize of fileSizes) {
fn: (textBuffer) => { fn: (textBuffer) => {
// for line model, this loop doesn't reflect the real situation. // for line model, this loop doesn't reflect the real situation.
for (const edit of edits) { for (const edit of edits) {
textBuffer.applyEdits([edit], false); textBuffer.applyEdits([edit], false, false);
} }
} }
}); });
@@ -67,7 +67,7 @@ for (let fileSize of fileSizes) {
}, },
preCycle: (textBuffer) => { preCycle: (textBuffer) => {
for (const edit of edits) { for (const edit of edits) {
textBuffer.applyEdits([edit], false); textBuffer.applyEdits([edit], false, false);
} }
return textBuffer; return textBuffer;
}, },
@@ -91,7 +91,7 @@ for (let fileSize of fileSizes) {
}, },
preCycle: (textBuffer) => { preCycle: (textBuffer) => {
for (const edit of edits) { for (const edit of edits) {
textBuffer.applyEdits([edit], false); textBuffer.applyEdits([edit], false, false);
} }
return textBuffer; return textBuffer;
}, },
@@ -121,7 +121,7 @@ for (let fileSize of fileSizes) {
}, },
preCycle: (textBuffer) => { preCycle: (textBuffer) => {
for (const edit of edits) { for (const edit of edits) {
textBuffer.applyEdits([edit], false); textBuffer.applyEdits([edit], false, false);
} }
return textBuffer; return textBuffer;
}, },
@@ -134,4 +134,4 @@ for (let fileSize of fileSizes) {
editsSuite.run(); editsSuite.run();
} }
} }

View File

@@ -41,10 +41,10 @@ for (let fileSize of fileSizes) {
return textBuffer; return textBuffer;
}, },
fn: (textBuffer) => { fn: (textBuffer) => {
textBuffer.applyEdits(edits.slice(0, i), false); textBuffer.applyEdits(edits.slice(0, i), false, false);
} }
}); });
} }
replaceSuite.run(); replaceSuite.run();
} }

View File

@@ -1104,7 +1104,7 @@ suite('EditorModel - EditableTextModel.applyEdits', () => {
{ range: new Range(3, 1, 3, 6), text: null, }, { range: new Range(3, 1, 3, 6), text: null, },
{ range: new Range(2, 1, 3, 1), text: null, }, { range: new Range(2, 1, 3, 1), text: null, },
{ range: new Range(3, 6, 3, 6), text: '\nline2' } { range: new Range(3, 6, 3, 6), text: '\nline2' }
]); ], true);
model.applyEdits(undoEdits); model.applyEdits(undoEdits);

View File

@@ -17,7 +17,7 @@ export function testApplyEditsWithSyncedModels(original: string[], edits: IIdent
assertSyncedModels(originalStr, (model, assertMirrorModels) => { assertSyncedModels(originalStr, (model, assertMirrorModels) => {
// Apply edits & collect inverse edits // Apply edits & collect inverse edits
let inverseEdits = model.applyEdits(edits); let inverseEdits = model.applyEdits(edits, true);
// Assert edits produced expected result // Assert edits produced expected result
assert.deepEqual(model.getValue(EndOfLinePreference.LF), expectedStr); assert.deepEqual(model.getValue(EndOfLinePreference.LF), expectedStr);
@@ -25,7 +25,7 @@ export function testApplyEditsWithSyncedModels(original: string[], edits: IIdent
assertMirrorModels(); assertMirrorModels();
// Apply the inverse edits // Apply the inverse edits
let inverseInverseEdits = model.applyEdits(inverseEdits); let inverseInverseEdits = model.applyEdits(inverseEdits, true);
// Assert the inverse edits brought back model to original state // Assert the inverse edits brought back model to original state
assert.deepEqual(model.getValue(EndOfLinePreference.LF), originalStr); assert.deepEqual(model.getValue(EndOfLinePreference.LF), originalStr);
@@ -36,8 +36,8 @@ export function testApplyEditsWithSyncedModels(original: string[], edits: IIdent
identifier: edit.identifier, identifier: edit.identifier,
range: edit.range, range: edit.range,
text: edit.text, text: edit.text,
forceMoveMarkers: edit.forceMoveMarkers, forceMoveMarkers: edit.forceMoveMarkers || false,
isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit || false
}; };
}; };
// Assert the inverse of the inverse edits are the original edits // Assert the inverse of the inverse edits are the original edits

View File

@@ -18,7 +18,10 @@ suite('PieceTreeTextBuffer._getInverseEdits', () => {
range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), range: new Range(startLineNumber, startColumn, endLineNumber, endColumn),
rangeOffset: 0, rangeOffset: 0,
rangeLength: 0, rangeLength: 0,
lines: text, text: text ? text.join('\n') : '',
eolCount: text ? text.length - 1 : 0,
firstLineLength: text ? text[0].length : 0,
lastLineLength: text ? text[text.length - 1].length : 0,
forceMoveMarkers: false, forceMoveMarkers: false,
isAutoWhitespaceEdit: false isAutoWhitespaceEdit: false
}; };
@@ -269,7 +272,10 @@ suite('PieceTreeTextBuffer._toSingleEditOperation', () => {
range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), range: new Range(startLineNumber, startColumn, endLineNumber, endColumn),
rangeOffset: rangeOffset, rangeOffset: rangeOffset,
rangeLength: rangeLength, rangeLength: rangeLength,
lines: text, text: text ? text.join('\n') : '',
eolCount: text ? text.length - 1 : 0,
firstLineLength: text ? text[0].length : 0,
lastLineLength: text ? text[text.length - 1].length : 0,
forceMoveMarkers: false, forceMoveMarkers: false,
isAutoWhitespaceEdit: false isAutoWhitespaceEdit: false
}; };

View File

@@ -330,7 +330,7 @@ suite('Editor Model - Model', () => {
let res = thisModel.applyEdits([ let res = thisModel.applyEdits([
{ range: new Range(2, 1, 2, 1), text: 'a' }, { range: new Range(2, 1, 2, 1), text: 'a' },
{ range: new Range(1, 1, 1, 1), text: 'b' }, { range: new Range(1, 1, 1, 1), text: 'b' },
]); ], true);
assert.deepEqual(res[0].range, new Range(2, 1, 2, 2)); assert.deepEqual(res[0].range, new Range(2, 1, 2, 2));
assert.deepEqual(res[1].range, new Range(1, 1, 1, 2)); assert.deepEqual(res[1].range, new Range(1, 1, 1, 2));

View File

@@ -50,14 +50,14 @@ suite('Editor Model - Model Edit Operation', () => {
function assertSingleEditOp(singleEditOp: IIdentifiedSingleEditOperation, editedLines: string[]) { function assertSingleEditOp(singleEditOp: IIdentifiedSingleEditOperation, editedLines: string[]) {
let editOp = [singleEditOp]; let editOp = [singleEditOp];
let inverseEditOp = model.applyEdits(editOp); let inverseEditOp = model.applyEdits(editOp, true);
assert.equal(model.getLineCount(), editedLines.length); assert.equal(model.getLineCount(), editedLines.length);
for (let i = 0; i < editedLines.length; i++) { for (let i = 0; i < editedLines.length; i++) {
assert.equal(model.getLineContent(i + 1), editedLines[i]); assert.equal(model.getLineContent(i + 1), editedLines[i]);
} }
let originalOp = model.applyEdits(inverseEditOp); let originalOp = model.applyEdits(inverseEditOp, true);
assert.equal(model.getLineCount(), 5); assert.equal(model.getLineCount(), 5);
assert.equal(model.getLineContent(1), LINE1); assert.equal(model.getLineContent(1), LINE1);
@@ -71,8 +71,8 @@ suite('Editor Model - Model Edit Operation', () => {
identifier: edit.identifier, identifier: edit.identifier,
range: edit.range, range: edit.range,
text: edit.text, text: edit.text,
forceMoveMarkers: edit.forceMoveMarkers, forceMoveMarkers: edit.forceMoveMarkers || false,
isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit || false
}; };
}; };
assert.deepEqual(originalOp.map(simplifyEdit), editOp.map(simplifyEdit)); assert.deepEqual(originalOp.map(simplifyEdit), editOp.map(simplifyEdit));

View File

@@ -0,0 +1,269 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { compressConsecutiveTextChanges, TextChange } from 'vs/editor/common/model/textChange';
const GENERATE_TESTS = false;
interface IGeneratedEdit {
offset: number;
length: number;
text: string;
}
suite('TextChangeCompressor', () => {
function getResultingContent(initialContent: string, edits: IGeneratedEdit[]): string {
let content = initialContent;
for (let i = edits.length - 1; i >= 0; i--) {
content = (
content.substring(0, edits[i].offset) +
edits[i].text +
content.substring(edits[i].offset + edits[i].length)
);
}
return content;
}
function getTextChanges(initialContent: string, edits: IGeneratedEdit[]): TextChange[] {
let content = initialContent;
let changes: TextChange[] = new Array<TextChange>(edits.length);
let deltaOffset = 0;
for (let i = 0; i < edits.length; i++) {
let edit = edits[i];
let position = edit.offset + deltaOffset;
let length = edit.length;
let text = edit.text;
let oldText = content.substr(position, length);
content = (
content.substr(0, position) +
text +
content.substr(position + length)
);
changes[i] = new TextChange(edit.offset, oldText, position, text);
deltaOffset += text.length - length;
}
return changes;
}
function assertCompression(initialText: string, edit1: IGeneratedEdit[], edit2: IGeneratedEdit[]): void {
let tmpText = getResultingContent(initialText, edit1);
let chg1 = getTextChanges(initialText, edit1);
let finalText = getResultingContent(tmpText, edit2);
let chg2 = getTextChanges(tmpText, edit2);
let compressedTextChanges = compressConsecutiveTextChanges(chg1, chg2);
// Check that the compression was correct
let compressedDoTextEdits: IGeneratedEdit[] = compressedTextChanges.map((change) => {
return {
offset: change.oldPosition,
length: change.oldLength,
text: change.newText
};
});
let actualDoResult = getResultingContent(initialText, compressedDoTextEdits);
assert.equal(actualDoResult, finalText);
let compressedUndoTextEdits: IGeneratedEdit[] = compressedTextChanges.map((change) => {
return {
offset: change.newPosition,
length: change.newLength,
text: change.oldText
};
});
let actualUndoResult = getResultingContent(finalText, compressedUndoTextEdits);
assert.equal(actualUndoResult, initialText);
}
test('simple 1', () => {
assertCompression(
'',
[{ offset: 0, length: 0, text: 'h' }],
[{ offset: 1, length: 0, text: 'e' }]
);
});
test('simple 2', () => {
assertCompression(
'|',
[{ offset: 0, length: 0, text: 'h' }],
[{ offset: 2, length: 0, text: 'e' }]
);
});
test('complex1', () => {
assertCompression(
'abcdefghij',
[
{ offset: 0, length: 3, text: 'qh' },
{ offset: 5, length: 0, text: '1' },
{ offset: 8, length: 2, text: 'X' }
],
[
{ offset: 1, length: 0, text: 'Z' },
{ offset: 3, length: 3, text: 'Y' },
]
);
});
test('gen1', () => {
assertCompression(
'kxm',
[{ offset: 0, length: 1, text: 'tod_neu' }],
[{ offset: 1, length: 2, text: 'sag_e' }]
);
});
test('gen2', () => {
assertCompression(
'kpb_r_v',
[{ offset: 5, length: 2, text: 'a_jvf_l' }],
[{ offset: 10, length: 2, text: 'w' }]
);
});
test('gen3', () => {
assertCompression(
'slu_w',
[{ offset: 4, length: 1, text: '_wfw' }],
[{ offset: 3, length: 5, text: '' }]
);
});
test('gen4', () => {
assertCompression(
'_e',
[{ offset: 2, length: 0, text: 'zo_b' }],
[{ offset: 1, length: 3, text: 'tra' }]
);
});
test('gen5', () => {
assertCompression(
'ssn_',
[{ offset: 0, length: 2, text: 'tat_nwe' }],
[{ offset: 2, length: 6, text: 'jm' }]
);
});
test('gen6', () => {
assertCompression(
'kl_nru',
[{ offset: 4, length: 1, text: '' }],
[{ offset: 1, length: 4, text: '__ut' }]
);
});
const _a = 'a'.charCodeAt(0);
const _z = 'z'.charCodeAt(0);
function getRandomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getRandomString(minLength: number, maxLength: number): string {
const length = getRandomInt(minLength, maxLength);
let r = '';
for (let i = 0; i < length; i++) {
r += String.fromCharCode(getRandomInt(_a, _z));
}
return r;
}
function getRandomEOL(): string {
switch (getRandomInt(1, 3)) {
case 1: return '\r';
case 2: return '\n';
case 3: return '\r\n';
}
throw new Error(`not possible`);
}
function getRandomBuffer(small: boolean): string {
let lineCount = getRandomInt(1, small ? 3 : 10);
let lines: string[] = [];
for (let i = 0; i < lineCount; i++) {
lines.push(getRandomString(0, small ? 3 : 10) + getRandomEOL());
}
return lines.join('');
}
function getRandomEdits(content: string, min: number = 1, max: number = 5): IGeneratedEdit[] {
let result: IGeneratedEdit[] = [];
let cnt = getRandomInt(min, max);
let maxOffset = content.length;
while (cnt > 0 && maxOffset > 0) {
let offset = getRandomInt(0, maxOffset);
let length = getRandomInt(0, maxOffset - offset);
let text = getRandomBuffer(true);
result.push({
offset: offset,
length: length,
text: text
});
maxOffset = offset;
cnt--;
}
result.reverse();
return result;
}
class GeneratedTest {
private readonly _content: string;
private readonly _edits1: IGeneratedEdit[];
private readonly _edits2: IGeneratedEdit[];
constructor() {
this._content = getRandomBuffer(false).replace(/\n/g, '_');
this._edits1 = getRandomEdits(this._content, 1, 5).map((e) => { return { offset: e.offset, length: e.length, text: e.text.replace(/\n/g, '_') }; });
let tmp = getResultingContent(this._content, this._edits1);
this._edits2 = getRandomEdits(tmp, 1, 5).map((e) => { return { offset: e.offset, length: e.length, text: e.text.replace(/\n/g, '_') }; });
}
public print(): void {
console.log(`assertCompression(${JSON.stringify(this._content)}, ${JSON.stringify(this._edits1)}, ${JSON.stringify(this._edits2)});`);
}
public assert(): void {
assertCompression(this._content, this._edits1, this._edits2);
}
}
if (GENERATE_TESTS) {
let testNumber = 0;
while (true) {
testNumber++;
console.log(`------RUNNING TextChangeCompressor TEST ${testNumber}`);
let test = new GeneratedTest();
try {
test.assert();
} catch (err) {
console.log(err);
test.print();
break;
}
}
}
});

View File

@@ -9,10 +9,11 @@ import * as platform from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { EditOperation } from 'vs/editor/common/core/editOperation'; import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Range } from 'vs/editor/common/core/range'; import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; import { createStringBuilder } from 'vs/editor/common/core/stringBuilder';
import { DefaultEndOfLine } from 'vs/editor/common/model'; import { DefaultEndOfLine } from 'vs/editor/common/model';
import { createTextBuffer } from 'vs/editor/common/model/textModel'; import { createTextBuffer } from 'vs/editor/common/model/textModel';
import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { ModelServiceImpl, MAINTAIN_UNDO_REDO_STACK } from 'vs/editor/common/services/modelServiceImpl';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
@@ -33,7 +34,8 @@ suite('ModelService', () => {
configService.setUserConfiguration('files', { 'eol': '\n' }); configService.setUserConfiguration('files', { 'eol': '\n' });
configService.setUserConfiguration('files', { 'eol': '\r\n' }, URI.file(platform.isWindows ? 'c:\\myroot' : '/myroot')); configService.setUserConfiguration('files', { 'eol': '\r\n' }, URI.file(platform.isWindows ? 'c:\\myroot' : '/myroot'));
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService(new TestDialogService(), new TestNotificationService())); const dialogService = new TestDialogService();
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService()), dialogService);
}); });
teardown(() => { teardown(() => {
@@ -307,6 +309,75 @@ suite('ModelService', () => {
]; ];
assertComputeEdits(file1, file2); assertComputeEdits(file1, file2);
}); });
if (MAINTAIN_UNDO_REDO_STACK) {
test('maintains undo for same resource and same content', () => {
const resource = URI.parse('file://test.txt');
// create a model
const model1 = modelService.createModel('text', null, resource);
// make an edit
model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]);
assert.equal(model1.getValue(), 'text1');
// dispose it
modelService.destroyModel(resource);
// create a new model with the same content
const model2 = modelService.createModel('text1', null, resource);
// undo
model2.undo();
assert.equal(model2.getValue(), 'text');
});
test('maintains version id and alternative version id for same resource and same content', () => {
const resource = URI.parse('file://test.txt');
// create a model
const model1 = modelService.createModel('text', null, resource);
// make an edit
model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]);
assert.equal(model1.getValue(), 'text1');
const versionId = model1.getVersionId();
const alternativeVersionId = model1.getAlternativeVersionId();
// dispose it
modelService.destroyModel(resource);
// create a new model with the same content
const model2 = modelService.createModel('text1', null, resource);
assert.equal(model2.getVersionId(), versionId);
assert.equal(model2.getAlternativeVersionId(), alternativeVersionId);
});
}
test('does not maintain undo for same resource and different content', () => {
const resource = URI.parse('file://test.txt');
// create a model
const model1 = modelService.createModel('text', null, resource);
// make an edit
model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]);
assert.equal(model1.getValue(), 'text1');
// dispose it
modelService.destroyModel(resource);
// create a new model with the same content
const model2 = modelService.createModel('text2', null, resource);
// undo
model2.undo();
assert.equal(model2.getValue(), 'text2');
});
test('setValue should clear undo stack', () => {
const resource = URI.parse('file://test.txt');
const model = modelService.createModel('text', null, resource);
model.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]);
assert.equal(model.getValue(), 'text1');
model.setValue('text2');
model.undo();
assert.equal(model.getValue(), 'text2');
});
}); });
function assertComputeEdits(lines1: string[], lines2: string[]): void { function assertComputeEdits(lines1: string[], lines2: string[]): void {

15
src/vs/monaco.d.ts vendored
View File

@@ -1552,14 +1552,9 @@ declare namespace monaco.editor {
*/ */
range: Range; range: Range;
/** /**
* The text to replace with. This can be null to emulate a simple delete. * The text to replace with. This can be empty to emulate a simple delete.
*/ */
text: string | null; text: string;
/**
* This indicates that this operation has "insert" semantics.
* i.e. forceMoveMarkers = true => if `range` is collapsed, all markers at the position will be moved.
*/
forceMoveMarkers: boolean;
} }
/** /**
@@ -1907,9 +1902,11 @@ declare namespace monaco.editor {
* Edit the model without adding the edits to the undo stack. * Edit the model without adding the edits to the undo stack.
* This can have dire consequences on the undo stack! See @pushEditOperations for the preferred way. * This can have dire consequences on the undo stack! See @pushEditOperations for the preferred way.
* @param operations The edit operations. * @param operations The edit operations.
* @return The inverse edit operations, that, when applied, will bring the model back to the previous state. * @return If desired, the inverse edit operations, that, when applied, will bring the model back to the previous state.
*/ */
applyEdits(operations: IIdentifiedSingleEditOperation[]): IValidEditOperation[]; applyEdits(operations: IIdentifiedSingleEditOperation[]): void;
applyEdits(operations: IIdentifiedSingleEditOperation[], computeUndoEdits: false): void;
applyEdits(operations: IIdentifiedSingleEditOperation[], computeUndoEdits: true): IValidEditOperation[];
/** /**
* Change the end of line sequence without recording in the undo stack. * Change the end of line sequence without recording in the undo stack.
* This can have dire consequences on the undo stack! See @pushEOL for the preferred way. * This can have dire consequences on the undo stack! See @pushEOL for the preferred way.

View File

@@ -125,6 +125,7 @@ export class MenuId {
static readonly TimelineItemContext = new MenuId('TimelineItemContext'); static readonly TimelineItemContext = new MenuId('TimelineItemContext');
static readonly TimelineTitle = new MenuId('TimelineTitle'); static readonly TimelineTitle = new MenuId('TimelineTitle');
static readonly TimelineTitleContext = new MenuId('TimelineTitleContext'); static readonly TimelineTitleContext = new MenuId('TimelineTitleContext');
static readonly AccountsContext = new MenuId('AccountsContext');
readonly id: number; readonly id: number;
readonly _debugName: string; readonly _debugName: string;

View File

@@ -332,7 +332,11 @@ export class ElectronMainService implements IElectronMainService {
} }
async closeWindow(windowId: number | undefined): Promise<void> { async closeWindow(windowId: number | undefined): Promise<void> {
const window = this.windowById(windowId); this.closeWindowById(windowId, windowId);
}
async closeWindowById(currentWindowId: number | undefined, targetWindowId?: number | undefined): Promise<void> {
const window = this.windowById(targetWindowId);
if (window) { if (window) {
return window.win.close(); return window.win.close();
} }

View File

@@ -74,6 +74,7 @@ export interface IElectronService {
relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise<void>; relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise<void>;
reload(options?: { disableExtensions?: boolean }): Promise<void>; reload(options?: { disableExtensions?: boolean }): Promise<void>;
closeWindow(): Promise<void>; closeWindow(): Promise<void>;
closeWindowById(windowId: number): Promise<void>;
quit(): Promise<void>; quit(): Promise<void>;
// Development // Development

View File

@@ -151,7 +151,7 @@ export class InstantiationService implements IInstantiationService {
graph.lookupOrInsertNode(item); graph.lookupOrInsertNode(item);
// a weak but working heuristic for cycle checks // a weak but working heuristic for cycle checks
if (cycleCount++ > 200) { if (cycleCount++ > 1000) {
throw new CyclicDependencyError(graph); throw new CyclicDependencyError(graph);
} }

View File

@@ -533,6 +533,7 @@ export class Menubar {
[ [
minimize, minimize,
zoom, zoom,
__separator__(),
switchWindow, switchWindow,
...nativeTabMenuItems, ...nativeTabMenuItems,
__separator__(), __separator__(),

View File

@@ -6,7 +6,6 @@
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { PickerQuickAccessProvider, IPickerQuickAccessItem, IPickerQuickAccessProviderOptions } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { PickerQuickAccessProvider, IPickerQuickAccessItem, IPickerQuickAccessProviderOptions } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { distinct } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { or, matchesPrefix, matchesWords, matchesContiguousSubString } from 'vs/base/common/filters'; import { or, matchesPrefix, matchesWords, matchesContiguousSubString } from 'vs/base/common/filters';
@@ -22,8 +21,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isPromiseCanceledError } from 'vs/base/common/errors'; import { isPromiseCanceledError } from 'vs/base/common/errors';
import { INotificationService } from 'vs/platform/notification/common/notification'; import { INotificationService } from 'vs/platform/notification/common/notification';
import { toErrorMessage } from 'vs/base/common/errorMessage'; import { toErrorMessage } from 'vs/base/common/errorMessage';
import { isFirefox } from 'vs/base/browser/browser';
import { timeout } from 'vs/base/common/async';
export interface ICommandQuickPick extends IPickerQuickAccessItem { export interface ICommandQuickPick extends IPickerQuickAccessItem {
commandId: string; commandId: string;
@@ -74,12 +71,9 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
} }
} }
// Remove duplicates
const distinctCommandPicks = distinct(filteredCommandPicks, pick => `${pick.label}${pick.commandId}`);
// Add description to commands that have duplicate labels // Add description to commands that have duplicate labels
const mapLabelToCommand = new Map<string, ICommandQuickPick>(); const mapLabelToCommand = new Map<string, ICommandQuickPick>();
for (const commandPick of distinctCommandPicks) { for (const commandPick of filteredCommandPicks) {
const existingCommandForLabel = mapLabelToCommand.get(commandPick.label); const existingCommandForLabel = mapLabelToCommand.get(commandPick.label);
if (existingCommandForLabel) { if (existingCommandForLabel) {
commandPick.description = commandPick.commandId; commandPick.description = commandPick.commandId;
@@ -90,7 +84,7 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
} }
// Sort by MRU order and fallback to name otherwise // Sort by MRU order and fallback to name otherwise
distinctCommandPicks.sort((commandPickA, commandPickB) => { filteredCommandPicks.sort((commandPickA, commandPickB) => {
const commandACounter = this.commandsHistory.peek(commandPickA.commandId); const commandACounter = this.commandsHistory.peek(commandPickA.commandId);
const commandBCounter = this.commandsHistory.peek(commandPickB.commandId); const commandBCounter = this.commandsHistory.peek(commandPickB.commandId);
@@ -113,8 +107,8 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
const commandPicks: Array<ICommandQuickPick | IQuickPickSeparator> = []; const commandPicks: Array<ICommandQuickPick | IQuickPickSeparator> = [];
let addSeparator = false; let addSeparator = false;
for (let i = 0; i < distinctCommandPicks.length; i++) { for (let i = 0; i < filteredCommandPicks.length; i++) {
const commandPick = distinctCommandPicks[i]; const commandPick = filteredCommandPicks[i];
const keybinding = this.keybindingService.lookupKeybinding(commandPick.commandId); const keybinding = this.keybindingService.lookupKeybinding(commandPick.commandId);
const ariaLabel = keybinding ? const ariaLabel = keybinding ?
localize('commandPickAriaLabelWithKeybinding', "{0}, {1}, commands picker", commandPick.label, keybinding.getAriaLabel()) : localize('commandPickAriaLabelWithKeybinding', "{0}, {1}, commands picker", commandPick.label, keybinding.getAriaLabel()) :
@@ -143,13 +137,6 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
// Add to history // Add to history
this.commandsHistory.push(commandPick.commandId); this.commandsHistory.push(commandPick.commandId);
if (!isFirefox) {
// Use a timeout to give the quick open widget a chance to close itself first
// Firefox: since the browser is quite picky for certain commands, we do not
// use a timeout (https://github.com/microsoft/vscode/issues/83288)
await timeout(50);
}
// Telementry // Telementry
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', {
id: commandPick.commandId, id: commandPick.commandId,
@@ -191,7 +178,7 @@ interface ICommandsQuickAccessConfiguration {
}; };
} }
class CommandsHistory extends Disposable { export class CommandsHistory extends Disposable {
static readonly DEFAULT_COMMANDS_HISTORY_LENGTH = 50; static readonly DEFAULT_COMMANDS_HISTORY_LENGTH = 50;

View File

@@ -66,6 +66,10 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider {
const editorProviders: IHelpQuickAccessPickItem[] = []; const editorProviders: IHelpQuickAccessPickItem[] = [];
for (const provider of this.registry.getQuickAccessProviders().sort((providerA, providerB) => providerA.prefix.localeCompare(providerB.prefix))) { for (const provider of this.registry.getQuickAccessProviders().sort((providerA, providerB) => providerA.prefix.localeCompare(providerB.prefix))) {
if (provider.prefix === HelpQuickAccessProvider.PREFIX) {
continue; // exclude help which is already active
}
for (const helpEntry of provider.helpEntries) { for (const helpEntry of provider.helpEntries) {
const prefix = helpEntry.prefix || provider.prefix; const prefix = helpEntry.prefix || provider.prefix;
const label = prefix || '\u2026' /* ... */; const label = prefix || '\u2026' /* ... */;

View File

@@ -25,7 +25,12 @@ export enum TriggerAction {
/** /**
* Update the results of the picker. * Update the results of the picker.
*/ */
REFRESH_PICKER REFRESH_PICKER,
/**
* Remove the item from the picker.
*/
REMOVE_ITEM
} }
export interface IPickerQuickAccessItem extends IQuickPickItem { export interface IPickerQuickAccessItem extends IQuickPickItem {
@@ -211,6 +216,14 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
case TriggerAction.REFRESH_PICKER: case TriggerAction.REFRESH_PICKER:
updatePickerItems(); updatePickerItems();
break; break;
case TriggerAction.REMOVE_ITEM:
const index = picker.items.indexOf(item);
if (index !== -1) {
const items = picker.items.slice();
items.splice(index, 1);
picker.items = items;
}
break;
} }
} }
} }

View File

@@ -5,18 +5,32 @@
import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { IQuickAccessController, IQuickAccessProvider, IQuickAccessRegistry, Extensions, IQuickAccessProviderDescriptor, IQuickAccessOptions } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickAccessController, IQuickAccessProvider, IQuickAccessRegistry, Extensions, IQuickAccessProviderDescriptor, IQuickAccessOptions, DefaultQuickAccessFilterValue } from 'vs/platform/quickinput/common/quickAccess';
import { Registry } from 'vs/platform/registry/common/platform'; import { Registry } from 'vs/platform/registry/common/platform';
import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { once } from 'vs/base/common/functional'; import { once } from 'vs/base/common/functional';
interface IInternalQuickAccessOptions extends IQuickAccessOptions {
/**
* Internal option to not rewrite the filter value at all but use it as is.
*/
preserveFilterValue?: boolean;
}
export class QuickAccessController extends Disposable implements IQuickAccessController { export class QuickAccessController extends Disposable implements IQuickAccessController {
private readonly registry = Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess); private readonly registry = Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess);
private readonly mapProviderToDescriptor = new Map<IQuickAccessProviderDescriptor, IQuickAccessProvider>(); private readonly mapProviderToDescriptor = new Map<IQuickAccessProviderDescriptor, IQuickAccessProvider>();
private lastActivePicker: IQuickPick<IQuickPickItem> | undefined = undefined; private readonly lastAcceptedPickerValues = new Map<IQuickAccessProviderDescriptor, string>();
private visibleQuickAccess: {
picker: IQuickPick<IQuickPickItem>,
descriptor: IQuickAccessProviderDescriptor | undefined,
value: string
} | undefined = undefined;
constructor( constructor(
@IQuickInputService private readonly quickInputService: IQuickInputService, @IQuickInputService private readonly quickInputService: IQuickInputService,
@@ -25,33 +39,131 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
super(); super();
} }
show(value = '', options?: IQuickAccessOptions): void { show(value = '', options?: IInternalQuickAccessOptions): void {
const disposables = new DisposableStore();
// Hide any previous picker if any
this.lastActivePicker?.hide();
// Find provider for the value to show // Find provider for the value to show
const [provider, descriptor] = this.getOrInstantiateProvider(value); const [provider, descriptor] = this.getOrInstantiateProvider(value);
// Return early if quick access is already showing on that
// same prefix and simply take over the filter value if it
// is more specific and select it for the user to be able
// to type over
const visibleQuickAccess = this.visibleQuickAccess;
const visibleDescriptor = visibleQuickAccess?.descriptor;
if (visibleQuickAccess && descriptor && visibleDescriptor === descriptor) {
// Take over the value only if it is not matching
// the existing provider prefix or we are to preserve
if (value !== descriptor.prefix && !options?.preserveFilterValue) {
visibleQuickAccess.picker.value = value;
}
// Always adjust selection
this.adjustValueSelection(visibleQuickAccess.picker, descriptor, options);
return;
}
// Rewrite the filter value based on certain rules unless disabled
if (descriptor && !options?.preserveFilterValue) {
let newValue: string | undefined = undefined;
// If we have a visible provider with a value, take it's filter value but
// rewrite to new provider prefix in case they differ
if (visibleQuickAccess && visibleDescriptor && visibleDescriptor !== descriptor) {
const newValueCandidateWithoutPrefix = visibleQuickAccess.value.substr(visibleDescriptor.prefix.length);
if (newValueCandidateWithoutPrefix) {
newValue = `${descriptor.prefix}${newValueCandidateWithoutPrefix}`;
}
}
// If the new provider wants to preserve the filter, take it's last remembered value
// If the new provider wants to define the filter, take it as is
if (!newValue) {
const defaultFilterValue = provider?.defaultFilterValue;
if (defaultFilterValue === DefaultQuickAccessFilterValue.LAST) {
newValue = this.lastAcceptedPickerValues.get(descriptor);
} else if (typeof defaultFilterValue === 'string') {
newValue = `${descriptor.prefix}${defaultFilterValue}`;
}
}
if (typeof newValue === 'string') {
value = newValue;
}
}
// Create a picker for the provider to use with the initial value // Create a picker for the provider to use with the initial value
// and adjust the filtering to exclude the prefix from filtering // and adjust the filtering to exclude the prefix from filtering
const disposables = new DisposableStore();
const picker = disposables.add(this.quickInputService.createQuickPick()); const picker = disposables.add(this.quickInputService.createQuickPick());
picker.placeholder = descriptor?.placeholder;
picker.value = value; picker.value = value;
this.adjustValueSelection(picker, descriptor, options);
picker.placeholder = descriptor?.placeholder;
picker.quickNavigate = options?.quickNavigateConfiguration; picker.quickNavigate = options?.quickNavigateConfiguration;
picker.valueSelection = options?.inputSelection ? [options.inputSelection.start, options.inputSelection.end] : [value.length, value.length]; picker.hideInput = !!picker.quickNavigate && !visibleQuickAccess; // only hide input if there was no picker opened already
picker.autoFocusSecondEntry = !!options?.quickNavigateConfiguration || !!options?.autoFocus?.autoFocusSecondEntry;
picker.contextKey = descriptor?.contextKey; picker.contextKey = descriptor?.contextKey;
picker.filterValue = (value: string) => value.substring(descriptor ? descriptor.prefix.length : 0); picker.filterValue = (value: string) => value.substring(descriptor ? descriptor.prefix.length : 0);
// Remember as last active picker and clean up once picker get's disposed // Register listeners
this.lastActivePicker = picker; const cancellationToken = this.registerPickerListeners(disposables, picker, provider, descriptor, value);
// Ask provider to fill the picker as needed if we have one
if (provider) {
disposables.add(provider.provide(picker, cancellationToken));
}
// Finally, show the picker. This is important because a provider
// may not call this and then our disposables would leak that rely
// on the onDidHide event.
picker.show();
}
private adjustValueSelection(picker: IQuickPick<IQuickPickItem>, descriptor?: IQuickAccessProviderDescriptor, options?: IInternalQuickAccessOptions): void {
let valueSelection: [number, number];
// Preserve: just always put the cursor at the end
if (options?.preserveFilterValue) {
valueSelection = [picker.value.length, picker.value.length];
}
// Otherwise: select the value up until the prefix
else {
valueSelection = [descriptor?.prefix.length ?? 0, picker.value.length];
}
picker.valueSelection = valueSelection;
}
private registerPickerListeners(disposables: DisposableStore, picker: IQuickPick<IQuickPickItem>, provider: IQuickAccessProvider | undefined, descriptor: IQuickAccessProviderDescriptor | undefined, value: string): CancellationToken {
// Remember as last visible picker and clean up once picker get's disposed
const visibleQuickAccess = this.visibleQuickAccess = { picker, descriptor, value };
disposables.add(toDisposable(() => { disposables.add(toDisposable(() => {
if (picker === this.lastActivePicker) { if (visibleQuickAccess === this.visibleQuickAccess) {
this.lastActivePicker = undefined; this.visibleQuickAccess = undefined;
} }
})); }));
// Whenever the value changes, check if the provider has
// changed and if so - re-create the picker from the beginning
disposables.add(picker.onDidChangeValue(value => {
const [providerForValue] = this.getOrInstantiateProvider(value);
if (providerForValue !== provider) {
this.show(value, { preserveFilterValue: true } /* do not rewrite value from user typing! */);
} else {
visibleQuickAccess.value = value; // remember the value in our visible one
}
}));
// Remember picker input for future use when accepting
if (descriptor) {
disposables.add(picker.onDidAccept(() => {
this.lastAcceptedPickerValues.set(descriptor, picker.value);
}));
}
// Create a cancellation token source that is valid as long as the // Create a cancellation token source that is valid as long as the
// picker has not been closed without picking an item // picker has not been closed without picking an item
const cts = disposables.add(new CancellationTokenSource()); const cts = disposables.add(new CancellationTokenSource());
@@ -64,24 +176,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
disposables.dispose(); disposables.dispose();
}); });
// Whenever the value changes, check if the provider has return cts.token;
// changed and if so - re-create the picker from the beginning
disposables.add(picker.onDidChangeValue(value => {
const [providerForValue] = this.getOrInstantiateProvider(value);
if (providerForValue !== provider) {
this.show(value);
}
}));
// Ask provider to fill the picker as needed if we have one
if (provider) {
disposables.add(provider.provide(picker, cts.token));
}
// Finally, show the picker. This is important because a provider
// may not call this and then our disposables would leak that rely
// on the onDidHide event.
picker.show();
} }
private getOrInstantiateProvider(value: string): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] { private getOrInstantiateProvider(value: string): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] {

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { IQuickInputService, IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInputButton, IInputBox, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { IQuickInputService, IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInputButton, IInputBox, QuickPickInput, IKeyMods } from 'vs/platform/quickinput/common/quickInput';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService';
@@ -154,8 +154,8 @@ export class QuickInputService extends Themable implements IQuickInputService {
this.controller.navigate(next, quickNavigate); this.controller.navigate(next, quickNavigate);
} }
accept() { accept(keyMods?: IKeyMods) {
return this.controller.accept(); return this.controller.accept(keyMods);
} }
back() { back() {

View File

@@ -12,15 +12,15 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
export interface IQuickAccessOptions { export interface IQuickAccessOptions {
/**
* Allows to control the part of text in the input field that should be selected.
*/
inputSelection?: { start: number; end: number; };
/** /**
* Allows to enable quick navigate support in quick input. * Allows to enable quick navigate support in quick input.
*/ */
quickNavigateConfiguration?: IQuickNavigateConfiguration; quickNavigateConfiguration?: IQuickNavigateConfiguration;
/**
* Wether to select the second pick item by default instead of the first.
*/
autoFocus?: { autoFocusSecondEntry?: boolean }
} }
export interface IQuickAccessController { export interface IQuickAccessController {
@@ -31,8 +31,32 @@ export interface IQuickAccessController {
show(value?: string, options?: IQuickAccessOptions): void; show(value?: string, options?: IQuickAccessOptions): void;
} }
export enum DefaultQuickAccessFilterValue {
/**
* Keep the value as it is given to quick access.
*/
PRESERVE = 0,
/**
* Use the value that was used last time something was accepted from the picker.
*/
LAST = 1
}
export interface IQuickAccessProvider { export interface IQuickAccessProvider {
/**
* Allows to set a default filter value when the provider opens. This can be:
* - `undefined` to not specify any default value
* - `DefaultFilterValues.PRESERVE` to use the value that was last typed
* - `string` for the actual value to use
*
* Note: the default filter will only be used if quick access was opened with
* the exact prefix of the provider. Otherwise the filter value is preserved.
*/
readonly defaultFilterValue?: string | DefaultQuickAccessFilterValue;
/** /**
* Called whenever a prefix was typed into quick pick that matches the provider. * Called whenever a prefix was typed into quick pick that matches the provider.
* *

View File

@@ -6,7 +6,7 @@
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInputButton, IInputBox, QuickPickInput } from 'vs/base/parts/quickinput/common/quickInput'; import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInputButton, IInputBox, QuickPickInput, IKeyMods } from 'vs/base/parts/quickinput/common/quickInput';
import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess';
export * from 'vs/base/parts/quickinput/common/quickInput'; export * from 'vs/base/parts/quickinput/common/quickInput';
@@ -84,8 +84,11 @@ export interface IQuickInputService {
/** /**
* Accept the selected item. * Accept the selected item.
*
* @param keyMods allows to override the state of key
* modifiers that should be present when invoking.
*/ */
accept(): Promise<void>; accept(keyMods?: IKeyMods): Promise<void>;
/** /**
* Cancels quick input and closes it. * Cancels quick input and closes it.

View File

@@ -30,6 +30,13 @@ export interface IWorkspaceUndoRedoElement {
split(): IResourceUndoRedoElement[]; split(): IResourceUndoRedoElement[];
} }
export type IUndoRedoElement = IResourceUndoRedoElement | IWorkspaceUndoRedoElement;
export interface IPastFutureElements {
past: IUndoRedoElement[];
future: IUndoRedoElement[];
}
export interface IUndoRedoService { export interface IUndoRedoService {
_serviceBrand: undefined; _serviceBrand: undefined;
@@ -37,12 +44,18 @@ export interface IUndoRedoService {
* Add a new element to the `undo` stack. * Add a new element to the `undo` stack.
* This will destroy the `redo` stack. * This will destroy the `redo` stack.
*/ */
pushElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement): void; pushElement(element: IUndoRedoElement): void;
/** /**
* Get the last pushed element. If the last pushed element has been undone, returns null. * Get the last pushed element. If the last pushed element has been undone, returns null.
*/ */
getLastElement(resource: URI): IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null; getLastElement(resource: URI): IUndoRedoElement | null;
getElements(resource: URI): IPastFutureElements;
hasElements(resource: URI): boolean;
setElementsIsValid(resource: URI, isValid: boolean): void;
/** /**
* Remove elements that target `resource`. * Remove elements that target `resource`.

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls'; import * as nls from 'vs/nls';
import { IUndoRedoService, IResourceUndoRedoElement, IWorkspaceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import { IUndoRedoService, IResourceUndoRedoElement, IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources'; import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources';
import { onUnexpectedError } from 'vs/base/common/errors'; import { onUnexpectedError } from 'vs/base/common/errors';
@@ -23,6 +23,7 @@ class ResourceStackElement {
public readonly strResource: string; public readonly strResource: string;
public readonly resources: URI[]; public readonly resources: URI[];
public readonly strResources: string[]; public readonly strResources: string[];
public isValid: boolean;
constructor(actual: IResourceUndoRedoElement) { constructor(actual: IResourceUndoRedoElement) {
this.actual = actual; this.actual = actual;
@@ -31,6 +32,11 @@ class ResourceStackElement {
this.strResource = uriGetComparisonKey(this.resource); this.strResource = uriGetComparisonKey(this.resource);
this.resources = [this.resource]; this.resources = [this.resource];
this.strResources = [this.strResource]; this.strResources = [this.strResource];
this.isValid = true;
}
public setValid(isValid: boolean): void {
this.isValid = isValid;
} }
} }
@@ -39,22 +45,57 @@ const enum RemovedResourceReason {
NoParallelUniverses = 1 NoParallelUniverses = 1
} }
class ResourceReasonPair {
constructor(
public readonly resource: URI,
public readonly reason: RemovedResourceReason
) { }
}
class RemovedResources { class RemovedResources {
public readonly set: Set<string> = new Set<string>(); private readonly elements = new Map<string, ResourceReasonPair>();
public readonly reason: [URI[], URI[]] = [[], []];
private _getPath(resource: URI): string {
return resource.scheme === Schemas.file ? resource.fsPath : resource.path;
}
public createMessage(): string { public createMessage(): string {
let messages: string[] = []; const externalRemoval: string[] = [];
if (this.reason[RemovedResourceReason.ExternalRemoval].length > 0) { const noParallelUniverses: string[] = [];
const paths = this.reason[RemovedResourceReason.ExternalRemoval].map(uri => uri.scheme === Schemas.file ? uri.fsPath : uri.path); for (const [, element] of this.elements) {
messages.push(nls.localize('externalRemoval', "The following files have been closed: {0}.", paths.join(', '))); const dest = (
element.reason === RemovedResourceReason.ExternalRemoval
? externalRemoval
: noParallelUniverses
);
dest.push(this._getPath(element.resource));
} }
if (this.reason[RemovedResourceReason.NoParallelUniverses].length > 0) {
const paths = this.reason[RemovedResourceReason.NoParallelUniverses].map(uri => uri.scheme === Schemas.file ? uri.fsPath : uri.path); let messages: string[] = [];
messages.push(nls.localize('noParallelUniverses', "The following files have been modified in an incompatible way: {0}.", paths.join(', '))); if (externalRemoval.length > 0) {
messages.push(nls.localize('externalRemoval', "The following files have been closed: {0}.", externalRemoval.join(', ')));
}
if (noParallelUniverses.length > 0) {
messages.push(nls.localize('noParallelUniverses', "The following files have been modified in an incompatible way: {0}.", noParallelUniverses.join(', ')));
} }
return messages.join('\n'); return messages.join('\n');
} }
public get size(): number {
return this.elements.size;
}
public has(strResource: string): boolean {
return this.elements.has(strResource);
}
public set(strResource: string, value: ResourceReasonPair): void {
this.elements.set(strResource, value);
}
public delete(strResource: string): boolean {
return this.elements.delete(strResource);
}
} }
class WorkspaceStackElement { class WorkspaceStackElement {
@@ -65,6 +106,7 @@ class WorkspaceStackElement {
public readonly resources: URI[]; public readonly resources: URI[];
public readonly strResources: string[]; public readonly strResources: string[];
public removedResources: RemovedResources | null; public removedResources: RemovedResources | null;
public invalidatedResources: RemovedResources | null;
constructor(actual: IWorkspaceUndoRedoElement) { constructor(actual: IWorkspaceUndoRedoElement) {
this.actual = actual; this.actual = actual;
@@ -72,18 +114,37 @@ class WorkspaceStackElement {
this.resources = actual.resources.slice(0); this.resources = actual.resources.slice(0);
this.strResources = this.resources.map(resource => uriGetComparisonKey(resource)); this.strResources = this.resources.map(resource => uriGetComparisonKey(resource));
this.removedResources = null; this.removedResources = null;
this.invalidatedResources = null;
} }
public removeResource(resource: URI, strResource: string, reason: RemovedResourceReason): void { public removeResource(resource: URI, strResource: string, reason: RemovedResourceReason): void {
if (!this.removedResources) { if (!this.removedResources) {
this.removedResources = new RemovedResources(); this.removedResources = new RemovedResources();
} }
if (!this.removedResources.set.has(strResource)) { if (!this.removedResources.has(strResource)) {
this.removedResources.set.add(strResource); this.removedResources.set(strResource, new ResourceReasonPair(resource, reason));
this.removedResources.reason[reason].push(resource); }
}
public setValid(resource: URI, strResource: string, isValid: boolean): void {
if (isValid) {
if (this.invalidatedResources) {
this.invalidatedResources.delete(strResource);
if (this.invalidatedResources.size === 0) {
this.invalidatedResources = null;
}
}
} else {
if (!this.invalidatedResources) {
this.invalidatedResources = new RemovedResources();
}
if (!this.invalidatedResources.has(strResource)) {
this.invalidatedResources.set(strResource, new ResourceReasonPair(resource, RemovedResourceReason.ExternalRemoval));
}
} }
} }
} }
type StackElement = ResourceStackElement | WorkspaceStackElement; type StackElement = ResourceStackElement | WorkspaceStackElement;
class ResourceEditStack { class ResourceEditStack {
@@ -110,7 +171,7 @@ export class UndoRedoService implements IUndoRedoService {
this._editStacks = new Map<string, ResourceEditStack>(); this._editStacks = new Map<string, ResourceEditStack>();
} }
public pushElement(_element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement): void { public pushElement(_element: IUndoRedoElement): void {
const element: StackElement = (_element.type === UndoRedoElementType.Resource ? new ResourceStackElement(_element) : new WorkspaceStackElement(_element)); const element: StackElement = (_element.type === UndoRedoElementType.Resource ? new ResourceStackElement(_element) : new WorkspaceStackElement(_element));
for (let i = 0, len = element.resources.length; i < len; i++) { for (let i = 0, len = element.resources.length; i < len; i++) {
const resource = element.resources[i]; const resource = element.resources[i];
@@ -131,11 +192,18 @@ export class UndoRedoService implements IUndoRedoService {
} }
} }
editStack.future = []; editStack.future = [];
if (editStack.past.length > 0) {
const lastElement = editStack.past[editStack.past.length - 1];
if (lastElement.type === UndoRedoElementType.Resource && !lastElement.isValid) {
// clear undo stack
editStack.past = [];
}
}
editStack.past.push(element); editStack.past.push(element);
} }
} }
public getLastElement(resource: URI): IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null { public getLastElement(resource: URI): IUndoRedoElement | null {
const strResource = uriGetComparisonKey(resource); const strResource = uriGetComparisonKey(resource);
if (this._editStacks.has(strResource)) { if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!; const editStack = this._editStacks.get(strResource)!;
@@ -150,7 +218,7 @@ export class UndoRedoService implements IUndoRedoService {
return null; return null;
} }
private _splitPastWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: Set<string> | null): void { private _splitPastWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: RemovedResources | null): void {
const individualArr = toRemove.actual.split(); const individualArr = toRemove.actual.split();
const individualMap = new Map<string, ResourceStackElement>(); const individualMap = new Map<string, ResourceStackElement>();
for (const _element of individualArr) { for (const _element of individualArr) {
@@ -178,7 +246,7 @@ export class UndoRedoService implements IUndoRedoService {
} }
} }
private _splitFutureWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: Set<string> | null): void { private _splitFutureWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: RemovedResources | null): void {
const individualArr = toRemove.actual.split(); const individualArr = toRemove.actual.split();
const individualMap = new Map<string, ResourceStackElement>(); const individualMap = new Map<string, ResourceStackElement>();
for (const _element of individualArr) { for (const _element of individualArr) {
@@ -224,6 +292,56 @@ export class UndoRedoService implements IUndoRedoService {
} }
} }
public setElementsIsValid(resource: URI, isValid: boolean): void {
const strResource = uriGetComparisonKey(resource);
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
for (const element of editStack.past) {
if (element.type === UndoRedoElementType.Workspace) {
element.setValid(resource, strResource, isValid);
} else {
element.setValid(isValid);
}
}
for (const element of editStack.future) {
if (element.type === UndoRedoElementType.Workspace) {
element.setValid(resource, strResource, isValid);
} else {
element.setValid(isValid);
}
}
}
}
// resource
public hasElements(resource: URI): boolean {
const strResource = uriGetComparisonKey(resource);
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
return (editStack.past.length > 0 || editStack.future.length > 0);
}
return false;
}
public getElements(resource: URI): IPastFutureElements {
const past: IUndoRedoElement[] = [];
const future: IUndoRedoElement[] = [];
const strResource = uriGetComparisonKey(resource);
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
for (const element of editStack.past) {
past.push(element.actual);
}
for (const element of editStack.future) {
future.push(element.actual);
}
}
return { past, future };
}
public canUndo(resource: URI): boolean { public canUndo(resource: URI): boolean {
const strResource = uriGetComparisonKey(resource); const strResource = uriGetComparisonKey(resource);
if (this._editStacks.has(strResource)) { if (this._editStacks.has(strResource)) {
@@ -257,11 +375,17 @@ export class UndoRedoService implements IUndoRedoService {
private _workspaceUndo(resource: URI, element: WorkspaceStackElement): Promise<void> | void { private _workspaceUndo(resource: URI, element: WorkspaceStackElement): Promise<void> | void {
if (element.removedResources) { if (element.removedResources) {
this._splitPastWorkspaceElement(element, element.removedResources.set); this._splitPastWorkspaceElement(element, element.removedResources);
const message = nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()); const message = nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage());
this._notificationService.info(message); this._notificationService.info(message);
return this.undo(resource); return this.undo(resource);
} }
if (element.invalidatedResources) {
this._splitPastWorkspaceElement(element, element.invalidatedResources);
const message = nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage());
this._notificationService.info(message);
return this.undo(resource);
}
// this must be the last past element in all the impacted resources! // this must be the last past element in all the impacted resources!
let affectedEditStacks: ResourceEditStack[] = []; let affectedEditStacks: ResourceEditStack[] = [];
@@ -313,6 +437,12 @@ export class UndoRedoService implements IUndoRedoService {
} }
private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement): Promise<void> | void { private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement): Promise<void> | void {
if (!element.isValid) {
// invalid element => immediately flush edit stack!
editStack.past = [];
editStack.future = [];
return;
}
editStack.past.pop(); editStack.past.pop();
editStack.future.push(element); editStack.future.push(element);
return this._safeInvoke(element, () => element.actual.undo()); return this._safeInvoke(element, () => element.actual.undo());
@@ -348,11 +478,17 @@ export class UndoRedoService implements IUndoRedoService {
private _workspaceRedo(resource: URI, element: WorkspaceStackElement): Promise<void> | void { private _workspaceRedo(resource: URI, element: WorkspaceStackElement): Promise<void> | void {
if (element.removedResources) { if (element.removedResources) {
this._splitFutureWorkspaceElement(element, element.removedResources.set); this._splitFutureWorkspaceElement(element, element.removedResources);
const message = nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()); const message = nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage());
this._notificationService.info(message); this._notificationService.info(message);
return this.redo(resource); return this.redo(resource);
} }
if (element.invalidatedResources) {
this._splitFutureWorkspaceElement(element, element.invalidatedResources);
const message = nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage());
this._notificationService.info(message);
return this.redo(resource);
}
// this must be the last future element in all the impacted resources! // this must be the last future element in all the impacted resources!
let affectedEditStacks: ResourceEditStack[] = []; let affectedEditStacks: ResourceEditStack[] = [];
@@ -383,6 +519,12 @@ export class UndoRedoService implements IUndoRedoService {
} }
private _resourceRedo(editStack: ResourceEditStack, element: ResourceStackElement): Promise<void> | void { private _resourceRedo(editStack: ResourceEditStack, element: ResourceStackElement): Promise<void> | void {
if (!element.isValid) {
// invalid element => immediately flush edit stack!
editStack.past = [];
editStack.future = [];
return;
}
editStack.future.pop(); editStack.future.pop();
editStack.past.push(element); editStack.past.push(element);
return this._safeInvoke(element, () => element.actual.redo()); return this._safeInvoke(element, () => element.actual.redo());

View File

@@ -7,9 +7,9 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer'; import { VSBuffer } from 'vs/base/common/buffer';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict } from 'vs/platform/userDataSync/common/userDataSync'; import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { joinPath, dirname, isEqual } from 'vs/base/common/resources'; import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
import { CancelablePromise } from 'vs/base/common/async'; import { CancelablePromise } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event'; import { Emitter, Event } from 'vs/base/common/event';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@@ -173,20 +173,36 @@ export abstract class AbstractSynchroniser extends Disposable {
return !!lastSyncData; return !!lastSyncData;
} }
async getConflictContent(conflictResource: URI): Promise<string | null> { async getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]> {
const handles = await this.userDataSyncStoreService.getAllRefs(this.resource);
return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) }));
}
async getLocalSyncResourceHandles(): Promise<ISyncResourceHandle[]> {
const handles = await this.userDataSyncBackupStoreService.getAllRefs(this.resource);
return handles.map(({ created, ref }) => ({ created, uri: this.toLocalBackupResource(ref) }));
}
private toRemoteBackupResource(ref: string): URI {
return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote-backup', path: `/${this.resource}/${ref}` });
}
private toLocalBackupResource(ref: string): URI {
return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${this.resource}/${ref}` });
}
async resolveContent(uri: URI): Promise<string | null> {
const ref = basename(uri);
if (isEqual(uri, this.toRemoteBackupResource(ref))) {
const { content } = await this.getUserData(ref);
return content;
}
if (isEqual(uri, this.toLocalBackupResource(ref))) {
return this.userDataSyncBackupStoreService.resolveContent(this.resource, ref);
}
return null; return null;
} }
async getRemoteContent(ref?: string): Promise<string | null> {
const refOrLastSyncUserData: string | IRemoteUserData | null = ref || await this.getLastSyncUserData();
const { content } = await this.getUserData(refOrLastSyncUserData);
return content;
}
async getLocalBackupContent(ref?: string): Promise<string | null> {
return this.userDataSyncBackupStoreService.resolveContent(this.resource, ref);
}
async resetLocal(): Promise<void> { async resetLocal(): Promise<void> {
try { try {
await this.fileService.del(this.lastSyncResource); await this.fileService.del(this.lastSyncResource);
@@ -265,9 +281,10 @@ export abstract class AbstractSynchroniser extends Disposable {
return this.userDataSyncBackupStoreService.backup(this.resource, JSON.stringify(syncData)); return this.userDataSyncBackupStoreService.backup(this.resource, JSON.stringify(syncData));
} }
abstract stop(): Promise<void>;
protected abstract readonly version: number; protected abstract readonly version: number;
protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus>; protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus>;
abstract stop(): Promise<void>;
} }
export interface IFileSyncPreviewResult { export interface IFileSyncPreviewResult {
@@ -310,7 +327,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
this.setStatus(SyncStatus.Idle); this.setStatus(SyncStatus.Idle);
} }
async getConflictContent(conflictResource: URI): Promise<string | null> { protected async getConflictContent(conflictResource: URI): Promise<string | null> {
if (isEqual(this.remotePreviewResource, conflictResource) || isEqual(this.localPreviewResource, conflictResource)) { if (isEqual(this.remotePreviewResource, conflictResource) || isEqual(this.localPreviewResource, conflictResource)) {
if (this.syncPreviewResultPromise) { if (this.syncPreviewResultPromise) {
const result = await this.syncPreviewResultPromise; const result = await this.syncPreviewResultPromise;

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
@@ -16,6 +16,9 @@ import { isNonEmptyArray } from 'vs/base/common/arrays';
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { joinPath, dirname, basename } from 'vs/base/common/resources';
import { format } from 'vs/base/common/jsonFormatter';
import { applyEdits } from 'vs/base/common/jsonEdit';
interface ISyncPreviewResult { interface ISyncPreviewResult {
readonly localExtensions: ISyncExtension[]; readonly localExtensions: ISyncExtension[];
@@ -120,28 +123,24 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
async stop(): Promise<void> { } async stop(): Promise<void> { }
async getRemoteContent(ref?: string, fragment?: string): Promise<string | null> { async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
const content = await super.getRemoteContent(ref); return [{ resource: joinPath(uri, 'extensions.json') }];
if (content !== null && fragment) {
return this.getFragment(content, fragment);
}
return content;
} }
async getLocalBackupContent(ref?: string, fragment?: string): Promise<string | null> { async resolveContent(uri: URI): Promise<string | null> {
let content = await super.getLocalBackupContent(ref); let content = await super.resolveContent(uri);
if (content !== null && fragment) { if (content) {
return this.getFragment(content, fragment); return content;
} }
return content; content = await super.resolveContent(dirname(uri));
} if (content) {
const syncData = this.parseSyncData(content);
private getFragment(content: string, fragment: string): string | null { if (syncData) {
const syncData = this.parseSyncData(content); switch (basename(uri)) {
if (syncData) { case 'extensions.json':
switch (fragment) { const edits = format(syncData.content, undefined, {});
case 'extensions': return applyEdits(syncData.content, edits);
return syncData.content; }
} }
} }
return null; return null;

View File

@@ -3,11 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer'; import { VSBuffer } from 'vs/base/common/buffer';
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { dirname } from 'vs/base/common/resources'; import { dirname, joinPath, basename } from 'vs/base/common/resources';
import { IFileService } from 'vs/platform/files/common/files'; import { IFileService } from 'vs/platform/files/common/files';
import { IStringDictionary } from 'vs/base/common/collections'; import { IStringDictionary } from 'vs/base/common/collections';
import { edit } from 'vs/platform/userDataSync/common/content'; import { edit } from 'vs/platform/userDataSync/common/content';
@@ -17,6 +17,8 @@ import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { format } from 'vs/base/common/jsonFormatter';
import { applyEdits } from 'vs/base/common/jsonEdit';
const argvProperties: string[] = ['locale']; const argvProperties: string[] = ['locale'];
@@ -105,28 +107,24 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
async stop(): Promise<void> { } async stop(): Promise<void> { }
async getRemoteContent(ref?: string, fragment?: string): Promise<string | null> { async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
let content = await super.getRemoteContent(ref); return [{ resource: joinPath(uri, 'globalState.json') }];
if (content !== null && fragment) {
return this.getFragment(content, fragment);
}
return content;
} }
async getLocalBackupContent(ref?: string, fragment?: string): Promise<string | null> { async resolveContent(uri: URI): Promise<string | null> {
let content = await super.getLocalBackupContent(ref); let content = await super.resolveContent(uri);
if (content !== null && fragment) { if (content) {
return this.getFragment(content, fragment); return content;
} }
return content; content = await super.resolveContent(dirname(uri));
} if (content) {
const syncData = this.parseSyncData(content);
private getFragment(content: string, fragment: string): string | null { if (syncData) {
const syncData = this.parseSyncData(content); switch (basename(uri)) {
if (syncData) { case 'globalState.json':
switch (fragment) { const edits = format(syncData.content, undefined, {});
case 'globalState': return applyEdits(syncData.content, edits);
return syncData.content; }
} }
} }
return null; return null;

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
import { VSBuffer } from 'vs/base/common/buffer'; import { VSBuffer } from 'vs/base/common/buffer';
import { parse } from 'vs/base/common/json'; import { parse } from 'vs/base/common/json';
@@ -19,7 +19,7 @@ import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { joinPath, isEqual } from 'vs/base/common/resources'; import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources';
interface ISyncContent { interface ISyncContent {
mac?: string; mac?: string;
@@ -160,38 +160,36 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
return false; return false;
} }
async getConflictContent(conflictResource: URI): Promise<string | null> { async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
const content = await super.getConflictContent(conflictResource); return [{ resource: joinPath(uri, 'keybindings.json'), comparableResource: this.file }];
return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null;
} }
async getRemoteContent(ref?: string, fragment?: string): Promise<string | null> { async resolveContent(uri: URI): Promise<string | null> {
const content = await super.getRemoteContent(ref); if (isEqual(this.remotePreviewResource, uri)) {
if (content !== null && fragment) { return this.getConflictContent(uri);
return this.getFragment(content, fragment);
} }
return content; let content = await super.resolveContent(uri);
} if (content) {
return content;
async getLocalBackupContent(ref?: string, fragment?: string): Promise<string | null> {
let content = await super.getLocalBackupContent(ref);
if (content !== null && fragment) {
return this.getFragment(content, fragment);
} }
return content; content = await super.resolveContent(dirname(uri));
} if (content) {
const syncData = this.parseSyncData(content);
private getFragment(content: string, fragment: string): string | null { if (syncData) {
const syncData = this.parseSyncData(content); switch (basename(uri)) {
if (syncData) { case 'keybindings.json':
switch (fragment) { return this.getKeybindingsContentFromSyncContent(syncData.content);
case 'keybindings': }
return this.getKeybindingsContentFromSyncContent(syncData.content);
} }
} }
return null; return null;
} }
protected async getConflictContent(conflictResource: URI): Promise<string | null> {
const content = await super.getConflictContent(conflictResource);
return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null;
}
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> { protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
try { try {
const result = await this.getPreview(remoteUserData, lastSyncUserData); const result = await this.getPreview(remoteUserData, lastSyncUserData);

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer'; import { VSBuffer } from 'vs/base/common/buffer';
import { parse } from 'vs/base/common/json'; import { parse } from 'vs/base/common/json';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
@@ -20,7 +20,7 @@ import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData }
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { joinPath, isEqual } from 'vs/base/common/resources'; import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources';
export interface ISettingsSyncContent { export interface ISettingsSyncContent {
settings: string; settings: string;
@@ -173,7 +173,35 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
return false; return false;
} }
async getConflictContent(conflictResource: URI): Promise<string | null> { async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
return [{ resource: joinPath(uri, 'settings.json'), comparableResource: this.file }];
}
async resolveContent(uri: URI): Promise<string | null> {
if (isEqual(this.remotePreviewResource, uri)) {
return this.getConflictContent(uri);
}
let content = await super.resolveContent(uri);
if (content) {
return content;
}
content = await super.resolveContent(dirname(uri));
if (content) {
const syncData = this.parseSyncData(content);
if (syncData) {
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
if (settingsSyncContent) {
switch (basename(uri)) {
case 'settings.json':
return settingsSyncContent.settings;
}
}
}
}
return null;
}
protected async getConflictContent(conflictResource: URI): Promise<string | null> {
let content = await super.getConflictContent(conflictResource); let content = await super.getConflictContent(conflictResource);
if (content !== null) { if (content !== null) {
const settingsSyncContent = this.parseSettingsSyncContent(content); const settingsSyncContent = this.parseSettingsSyncContent(content);
@@ -188,36 +216,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
return content; return content;
} }
async getRemoteContent(ref?: string, fragment?: string): Promise<string | null> {
let content = await super.getRemoteContent(ref);
if (content !== null && fragment) {
return this.getFragment(content, fragment);
}
return content;
}
async getLocalBackupContent(ref?: string, fragment?: string): Promise<string | null> {
let content = await super.getLocalBackupContent(ref);
if (content !== null && fragment) {
return this.getFragment(content, fragment);
}
return content;
}
private getFragment(content: string, fragment: string): string | null {
const syncData = this.parseSyncData(content);
if (syncData) {
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
if (settingsSyncContent) {
switch (fragment) {
case 'settings':
return settingsSyncContent.settings;
}
}
}
return null;
}
async acceptConflict(conflict: URI, content: string): Promise<void> { async acceptConflict(conflict: URI, content: string): Promise<void> {
if (this.status === SyncStatus.HasConflicts if (this.status === SyncStatus.HasConflicts
&& (isEqual(this.localPreviewResource, conflict) || isEqual(this.remotePreviewResource, conflict)) && (isEqual(this.localPreviewResource, conflict) || isEqual(this.remotePreviewResource, conflict))

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, UserDataSyncError, UserDataSyncErrorCode } from 'vs/platform/userDataSync/common/userDataSync'; import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, UserDataSyncError, UserDataSyncErrorCode, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -11,7 +11,7 @@ import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/us
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IStringDictionary } from 'vs/base/common/collections'; import { IStringDictionary } from 'vs/base/common/collections';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { joinPath, extname, relativePath, isEqualOrParent, isEqual, basename } from 'vs/base/common/resources'; import { joinPath, extname, relativePath, isEqualOrParent, isEqual, basename, dirname } from 'vs/base/common/resources';
import { VSBuffer } from 'vs/base/common/buffer'; import { VSBuffer } from 'vs/base/common/buffer';
import { merge } from 'vs/platform/userDataSync/common/snippetsMerge'; import { merge } from 'vs/platform/userDataSync/common/snippetsMerge';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
@@ -148,8 +148,46 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
this.setStatus(SyncStatus.Idle); this.setStatus(SyncStatus.Idle);
} }
async getConflictContent(conflictResource: URI): Promise<string | null> { async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
if (isEqualOrParent(conflictResource.with({ scheme: this.syncFolder.scheme }), this.snippetsPreviewFolder) && this.syncPreviewResultPromise) { let content = await super.resolveContent(uri);
if (content) {
const syncData = this.parseSyncData(content);
if (syncData) {
const snippets = this.parseSnippets(syncData);
const result = [];
for (const snippet of Object.keys(snippets)) {
const resource = joinPath(uri, snippet);
const comparableResource = joinPath(this.snippetsFolder, snippet);
const exists = await this.fileService.exists(comparableResource);
result.push({ resource, comparableResource: exists ? comparableResource : undefined });
}
return result;
}
}
return [];
}
async resolveContent(uri: URI): Promise<string | null> {
if (isEqualOrParent(uri.with({ scheme: this.syncFolder.scheme }), this.snippetsPreviewFolder)) {
return this.getConflictContent(uri);
}
let content = await super.resolveContent(uri);
if (content) {
return content;
}
content = await super.resolveContent(dirname(uri));
if (content) {
const syncData = this.parseSyncData(content);
if (syncData) {
const snippets = this.parseSnippets(syncData);
return snippets[basename(uri)] || null;
}
}
return null;
}
protected async getConflictContent(conflictResource: URI): Promise<string | null> {
if (this.syncPreviewResultPromise) {
const result = await this.syncPreviewResultPromise; const result = await this.syncPreviewResultPromise;
const key = relativePath(this.snippetsPreviewFolder, conflictResource.with({ scheme: this.snippetsPreviewFolder.scheme }))!; const key = relativePath(this.snippetsPreviewFolder, conflictResource.with({ scheme: this.snippetsPreviewFolder.scheme }))!;
if (conflictResource.scheme === this.snippetsPreviewFolder.scheme) { if (conflictResource.scheme === this.snippetsPreviewFolder.scheme) {
@@ -162,37 +200,6 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
return null; return null;
} }
async getRemoteContent(ref?: string, fragment?: string): Promise<string | null> {
const content = await super.getRemoteContent(ref);
if (content !== null && fragment) {
return this.getFragment(content, fragment);
}
return content;
}
async getLocalBackupContent(ref?: string, fragment?: string): Promise<string | null> {
let content = await super.getLocalBackupContent(ref);
if (content !== null && fragment) {
return this.getFragment(content, fragment);
}
return content;
}
private getFragment(content: string, fragment: string): string | null {
const syncData = this.parseSyncData(content);
return syncData ? this.getFragmentFromSyncData(syncData, fragment) : null;
}
private getFragmentFromSyncData(syncData: ISyncData, fragment: string): string | null {
switch (fragment) {
case 'snippets':
return syncData.content;
default:
const remoteSnippets = this.parseSnippets(syncData);
return remoteSnippets[fragment] || null;
}
}
async acceptConflict(conflictResource: URI, content: string): Promise<void> { async acceptConflict(conflictResource: URI, content: string): Promise<void> {
const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0]; const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0];
if (this.status === SyncStatus.HasConflicts && conflict) { if (this.status === SyncStatus.HasConflicts && conflict) {

View File

@@ -18,7 +18,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IStringDictionary } from 'vs/base/common/collections'; import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { joinPath, dirname, basename, isEqualOrParent } from 'vs/base/common/resources'; import { joinPath, isEqualOrParent } from 'vs/base/common/resources';
import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IProductService } from 'vs/platform/product/common/productService'; import { IProductService } from 'vs/platform/product/common/productService';
import { distinct } from 'vs/base/common/arrays'; import { distinct } from 'vs/base/common/arrays';
@@ -243,6 +243,11 @@ export const enum SyncStatus {
HasConflicts = 'hasConflicts', HasConflicts = 'hasConflicts',
} }
export interface ISyncResourceHandle {
created: number;
uri: URI;
}
export type Conflict = { remote: URI, local: URI }; export type Conflict = { remote: URI, local: URI };
export interface IUserDataSynchroniser { export interface IUserDataSynchroniser {
@@ -263,11 +268,12 @@ export interface IUserDataSynchroniser {
hasLocalData(): Promise<boolean>; hasLocalData(): Promise<boolean>;
resetLocal(): Promise<void>; resetLocal(): Promise<void>;
getConflictContent(conflictResource: URI): Promise<string | null>; resolveContent(resource: URI): Promise<string | null>;
acceptConflict(conflictResource: URI, content: string): Promise<void>; acceptConflict(conflictResource: URI, content: string): Promise<void>;
getRemoteContent(ref?: string, fragment?: string): Promise<string | null>; getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
getLocalBackupContent(ref?: string, fragment?: string): Promise<string | null>; getLocalSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>;
} }
//#endregion //#endregion
@@ -315,6 +321,10 @@ export interface IUserDataSyncService {
isFirstTimeSyncWithMerge(): Promise<boolean>; isFirstTimeSyncWithMerge(): Promise<boolean>;
resolveContent(resource: URI): Promise<string | null>; resolveContent(resource: URI): Promise<string | null>;
acceptConflict(conflictResource: URI, content: string): Promise<void>; acceptConflict(conflictResource: URI, content: string): Promise<void>;
getLocalSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
getRemoteSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>;
} }
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService'); export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
@@ -347,25 +357,6 @@ export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync';
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized); export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey<boolean>('syncEnabled', false); export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey<boolean>('syncEnabled', false);
export function toRemoteBackupSyncResource(resource: SyncResource, ref?: string): URI {
return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote-backup', path: `/${resource}/${ref ? ref : 'latest'}` });
}
export function toLocalBackupSyncResource(resource: SyncResource, ref?: string): URI {
return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${resource}/${ref ? ref : 'latest'}` });
}
export function resolveBackupSyncResource(resource: URI): { remote: boolean, resource: SyncResource, path: string } | null {
if (resource.scheme === USER_DATA_SYNC_SCHEME
&& resource.authority === 'remote-backup' || resource.authority === 'local-backup') {
const resourceKey: SyncResource = basename(dirname(resource)) as SyncResource;
const path = resource.path.substring(resourceKey.length + 1);
if (resourceKey && path) {
const remote = resource.authority === 'remote-backup';
return { remote, resource: resourceKey, path };
}
}
return null;
}
export const PREVIEW_DIR_NAME = 'preview'; export const PREVIEW_DIR_NAME = 'preview';
export function getSyncResourceFromLocalPreview(localPreview: URI, environmentService: IEnvironmentService): SyncResource | undefined { export function getSyncResourceFromLocalPreview(localPreview: URI, environmentService: IEnvironmentService): SyncResource | undefined {
if (localPreview.scheme === USER_DATA_SYNC_SCHEME) { if (localPreview.scheme === USER_DATA_SYNC_SCHEME) {

View File

@@ -5,7 +5,7 @@
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IUserDataSyncStoreService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { IStringDictionary } from 'vs/base/common/collections'; import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { FormattingOptions } from 'vs/base/common/jsonFormatter';
@@ -28,14 +28,17 @@ export class UserDataSyncChannel implements IServerChannel {
call(context: any, command: string, args?: any): Promise<any> { call(context: any, command: string, args?: any): Promise<any> {
switch (command) { switch (command) {
case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflicts, this.service.lastSyncTime]); case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflicts, this.service.lastSyncTime]);
case 'sync': return this.service.sync();
case 'acceptConflict': return this.service.acceptConflict(URI.revive(args[0]), args[1]);
case 'pull': return this.service.pull(); case 'pull': return this.service.pull();
case 'sync': return this.service.sync();
case 'stop': this.service.stop(); return Promise.resolve(); case 'stop': this.service.stop(); return Promise.resolve();
case 'reset': return this.service.reset(); case 'reset': return this.service.reset();
case 'resetLocal': return this.service.resetLocal(); case 'resetLocal': return this.service.resetLocal();
case 'resolveContent': return this.service.resolveContent(URI.revive(args[0]));
case 'isFirstTimeSyncWithMerge': return this.service.isFirstTimeSyncWithMerge(); case 'isFirstTimeSyncWithMerge': return this.service.isFirstTimeSyncWithMerge();
case 'acceptConflict': return this.service.acceptConflict(URI.revive(args[0]), args[1]);
case 'resolveContent': return this.service.resolveContent(URI.revive(args[0]));
case 'getLocalSyncResourceHandles': return this.service.getLocalSyncResourceHandles(args[0]);
case 'getRemoteSyncResourceHandles': return this.service.getRemoteSyncResourceHandles(args[0]);
case 'getAssociatedResources': return this.service.getAssociatedResources(args[0], { created: args[1].created, uri: URI.revive(args[1].uri) });
} }
throw new Error('Invalid call'); throw new Error('Invalid call');
} }
@@ -98,38 +101,3 @@ export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService {
} }
} }
export class UserDataSyncStoreServiceChannel implements IServerChannel {
constructor(private readonly service: IUserDataSyncStoreService) { }
listen(_: unknown, event: string): Event<any> {
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'getAllRefs': return this.service.getAllRefs(args[0]);
case 'resolveContent': return this.service.resolveContent(args[0], args[1]);
case 'delete': return this.service.delete(args[0]);
}
throw new Error('Invalid call');
}
}
export class UserDataSyncBackupStoreServiceChannel implements IServerChannel {
constructor(private readonly service: IUserDataSyncBackupStoreService) { }
listen(_: unknown, event: string): Event<any> {
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'getAllRefs': return this.service.getAllRefs(args[0]);
case 'resolveContent': return this.service.resolveContent(args[0], args[1]);
}
throw new Error('Invalid call');
}
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError, resolveBackupSyncResource, SyncResourceConflicts } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Emitter, Event } from 'vs/base/common/event'; import { Emitter, Event } from 'vs/base/common/event';
@@ -188,25 +188,27 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
} }
async resolveContent(resource: URI): Promise<string | null> { async resolveContent(resource: URI): Promise<string | null> {
const result = resolveBackupSyncResource(resource); for (const synchroniser of this.synchronisers) {
if (result) { const content = await synchroniser.resolveContent(resource);
const synchronizer = this.synchronisers.filter(s => s.resource === result.resource)[0]; if (content) {
if (synchronizer) {
const ref = result.path !== 'latest' ? result.path : undefined;
return result.remote ? synchronizer.getRemoteContent(ref, resource.fragment) : synchronizer.getLocalBackupContent(ref, resource.fragment);
}
}
for (const synchronizer of this.synchronisers) {
const content = await synchronizer.getConflictContent(resource);
if (content !== null) {
return content; return content;
} }
} }
return null; return null;
} }
getRemoteSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]> {
return this.getSynchroniser(resource).getRemoteSyncResourceHandles();
}
getLocalSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]> {
return this.getSynchroniser(resource).getLocalSyncResourceHandles();
}
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
return this.getSynchroniser(resource).getAssociatedResources(syncResourceHandle);
}
async isFirstTimeSyncWithMerge(): Promise<boolean> { async isFirstTimeSyncWithMerge(): Promise<boolean> {
await this.checkEnablement(); await this.checkEnablement();
if (!await this.userDataSyncStoreService.manifest()) { if (!await this.userDataSyncStoreService.manifest()) {

View File

@@ -40,6 +40,26 @@ declare module 'vscode' {
readonly removed: string[]; readonly removed: string[];
} }
/**
* An [event](#Event) which fires when an [AuthenticationSession](#AuthenticationSession) is added, removed, or changed.
*/
export interface AuthenticationSessionsChangeEvent {
/**
* The ids of the [AuthenticationSession](#AuthenticationSession)s that have been added.
*/
readonly added: string[];
/**
* The ids of the [AuthenticationSession](#AuthenticationSession)s that have been removed.
*/
readonly removed: string[];
/**
* The ids of the [AuthenticationSession](#AuthenticationSession)s that have been changed.
*/
readonly changed: string[];
}
export interface AuthenticationProvider { export interface AuthenticationProvider {
/** /**
* Used as an identifier for extensions trying to work with a particular * Used as an identifier for extensions trying to work with a particular
@@ -53,7 +73,7 @@ declare module 'vscode' {
* An [event](#Event) which fires when the array of sessions has changed, or data * An [event](#Event) which fires when the array of sessions has changed, or data
* within a session has changed. * within a session has changed.
*/ */
readonly onDidChangeSessions: Event<void>; readonly onDidChangeSessions: Event<AuthenticationSessionsChangeEvent>;
/** /**
* Returns an array of current sessions. * Returns an array of current sessions.
@@ -99,7 +119,7 @@ declare module 'vscode' {
* within a session has changed for a provider. Fires with the ids of the providers * within a session has changed for a provider. Fires with the ids of the providers
* that have had session data change. * that have had session data change.
*/ */
export const onDidChangeSessions: Event<string[]>; export const onDidChangeSessions: Event<{ [providerId: string]: AuthenticationSessionsChangeEvent }>;
} }
//#endregion //#endregion
@@ -1269,17 +1289,11 @@ declare module 'vscode' {
//#region Custom editors: https://github.com/microsoft/vscode/issues/77131 //#region Custom editors: https://github.com/microsoft/vscode/issues/77131
// TODO:
// - Think about where a rename would live.
// - Think about handling go to line? (add other editor options? reveal?)
// - Should we expose edits?
// - More properties from `TextDocument`?
/** /**
* Defines the editing capability of a custom webview editor. This allows the webview editor to hook into standard * Defines the editing capability of a custom webview editor. This allows the webview editor to hook into standard
* editor events such as `undo` or `save`. * editor events such as `undo` or `save`.
* *
* @param EditType Type of edits. * @param EditType Type of edits used for the documents this delegate handles.
*/ */
interface CustomEditorEditingDelegate<EditType = unknown> { interface CustomEditorEditingDelegate<EditType = unknown> {
/** /**
@@ -1290,7 +1304,7 @@ declare module 'vscode' {
* *
* @return Thenable signaling that the save has completed. * @return Thenable signaling that the save has completed.
*/ */
save(document: CustomDocument, cancellation: CancellationToken): Thenable<void>; save(document: CustomDocument<EditType>, cancellation: CancellationToken): Thenable<void>;
/** /**
* Save the existing resource at a new path. * Save the existing resource at a new path.
@@ -1300,7 +1314,7 @@ declare module 'vscode' {
* *
* @return Thenable signaling that the save has completed. * @return Thenable signaling that the save has completed.
*/ */
saveAs(document: CustomDocument, targetResource: Uri): Thenable<void>; saveAs(document: CustomDocument<EditType>, targetResource: Uri): Thenable<void>;
/** /**
* Event triggered by extensions to signal to VS Code that an edit has occurred. * Event triggered by extensions to signal to VS Code that an edit has occurred.
@@ -1317,7 +1331,7 @@ declare module 'vscode' {
* *
* @return Thenable signaling that the change has completed. * @return Thenable signaling that the change has completed.
*/ */
applyEdits(document: CustomDocument, edits: readonly EditType[]): Thenable<void>; applyEdits(document: CustomDocument<EditType>, edits: readonly EditType[]): Thenable<void>;
/** /**
* Undo a set of edits. * Undo a set of edits.
@@ -1329,7 +1343,7 @@ declare module 'vscode' {
* *
* @return Thenable signaling that the change has completed. * @return Thenable signaling that the change has completed.
*/ */
undoEdits(document: CustomDocument, edits: readonly EditType[]): Thenable<void>; undoEdits(document: CustomDocument<EditType>, edits: readonly EditType[]): Thenable<void>;
/** /**
* Revert the file to its last saved state. * Revert the file to its last saved state.
@@ -1339,7 +1353,7 @@ declare module 'vscode' {
* *
* @return Thenable signaling that the change has completed. * @return Thenable signaling that the change has completed.
*/ */
revert(document: CustomDocument, edits: CustomDocumentRevert<EditType>): Thenable<void>; revert(document: CustomDocument<EditType>, edits: CustomDocumentRevert<EditType>): Thenable<void>;
/** /**
* Back up the resource in its current state. * Back up the resource in its current state.
@@ -1360,22 +1374,25 @@ declare module 'vscode' {
* in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather * in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather
* than cancelling it to ensure that VS Code has some valid backup. * than cancelling it to ensure that VS Code has some valid backup.
*/ */
backup(document: CustomDocument, cancellation: CancellationToken): Thenable<void>; backup(document: CustomDocument<EditType>, cancellation: CancellationToken): Thenable<void>;
} }
/** /**
* Event triggered by extensions to signal to VS Code that an edit has occurred on a CustomDocument``. * Event triggered by extensions to signal to VS Code that an edit has occurred on a `CustomDocument`.
*
* @param EditType Type of edits used for the document.
*/ */
interface CustomDocumentEditEvent<EditType = unknown> { interface CustomDocumentEditEvent<EditType = unknown> {
/** /**
* Document the edit is for. * Document the edit is for.
*/ */
readonly document: CustomDocument; readonly document: CustomDocument<EditType>;
/** /**
* Object that describes the edit. * Object that describes the edit.
* *
* Edit objects are passed back to your extension in `undoEdits`, `applyEdits`, and `revert`. * Edit objects are passed back to your extension in `CustomEditorEditingDelegate.undoEdits`,
* `CustomEditorEditingDelegate.applyEdits`, and `CustomEditorEditingDelegate.revert`.
*/ */
readonly edit: EditType; readonly edit: EditType;
@@ -1403,13 +1420,19 @@ declare module 'vscode' {
/** /**
* Represents a custom document used by a `CustomEditorProvider`. * Represents a custom document used by a `CustomEditorProvider`.
* *
* Custom documents are only used within a given `CustomEditorProvider`. The lifecycle of a * All custom documents must subclass `CustomDocument`. Custom documents are only used within a given
* `CustomDocument` is managed by VS Code. When no more references remain to a given `CustomDocument`, * `CustomEditorProvider`. The lifecycle of a `CustomDocument` is managed by VS Code. When no more references
* then it is disposed of. * remain to a `CustomDocument`, it is disposed of.
* *
* @param UserDataType Type of custom object that extensions can store on the document. * @param EditType Type of edits used in this document.
*/ */
interface CustomDocument<UserDataType = unknown> { class CustomDocument<EditType = unknown> {
/**
* @param viewType The associated uri for this document.
* @param uri The associated viewType for this document.
*/
constructor(viewType: string, uri: Uri);
/** /**
* The associated viewType for this document. * The associated viewType for this document.
*/ */
@@ -1426,12 +1449,17 @@ declare module 'vscode' {
readonly onDidDispose: Event<void>; readonly onDidDispose: Event<void>;
/** /**
* Custom data that an extension can store on the document. * List of edits from document open to the document's current state.
*/ */
userData?: UserDataType; readonly appliedEdits: ReadonlyArray<EditType>;
// TODO: Should we expose edits here? /**
// This could be helpful for tracking the life cycle of edits * List of edits from document open to the document's last saved point.
*
* The save point will be behind `appliedEdits` if the user saves and then continues editing,
* or in front of the last entry in `appliedEdits` if the user saves and then hits undo.
*/
readonly savedEdits: ReadonlyArray<EditType>;
} }
/** /**
@@ -1443,7 +1471,8 @@ declare module 'vscode' {
* You should use custom text based editors when dealing with binary files or more complex scenarios. For simple text * You should use custom text based editors when dealing with binary files or more complex scenarios. For simple text
* based documents, use [`WebviewTextEditorProvider`](#WebviewTextEditorProvider) instead. * based documents, use [`WebviewTextEditorProvider`](#WebviewTextEditorProvider) instead.
*/ */
export interface CustomEditorProvider { export interface CustomEditorProvider<EditType = unknown> {
/** /**
* Resolve the model for a given resource. * Resolve the model for a given resource.
* *
@@ -1452,18 +1481,18 @@ declare module 'vscode' {
* If all editors for a given resource are closed, the `CustomDocument` is disposed of. Opening an editor at * If all editors for a given resource are closed, the `CustomDocument` is disposed of. Opening an editor at
* this point will trigger another call to `resolveCustomDocument`. * this point will trigger another call to `resolveCustomDocument`.
* *
* @param document Document to resolve. * @param uri Uri of the document to open.
* @param token A cancellation token that indicates the result is no longer needed. * @param token A cancellation token that indicates the result is no longer needed.
* *
* @return The capabilities of the resolved document. * @return The custom document.
*/ */
resolveCustomDocument(document: CustomDocument, token: CancellationToken): Thenable<void>; // TODO: rename to open? openCustomDocument(uri: Uri, token: CancellationToken): Thenable<CustomDocument<EditType>>;
/** /**
* Resolve a webview editor for a given resource. * Resolve a webview editor for a given resource.
* *
* This is called when a user first opens a resource for a `CustomTextEditorProvider`, or if they reopen an * This is called when a user first opens a resource for a `CustomEditorProvider`, or if they reopen an
* existing editor using this `CustomTextEditorProvider`. * existing editor using this `CustomEditorProvider`.
* *
* To resolve a webview editor, the provider must fill in its initial html content and hook up all * To resolve a webview editor, the provider must fill in its initial html content and hook up all
* the event listeners it is interested it. The provider can also hold onto the `WebviewPanel` to use later, * the event listeners it is interested it. The provider can also hold onto the `WebviewPanel` to use later,
@@ -1475,14 +1504,14 @@ declare module 'vscode' {
* *
* @return Thenable indicating that the webview editor has been resolved. * @return Thenable indicating that the webview editor has been resolved.
*/ */
resolveCustomEditor(document: CustomDocument, webviewPanel: WebviewPanel, token: CancellationToken): Thenable<void>; resolveCustomEditor(document: CustomDocument<EditType>, webviewPanel: WebviewPanel, token: CancellationToken): Thenable<void>;
/** /**
* Defines the editing capability of a custom webview document. * Defines the editing capability of a custom webview document.
* *
* When not provided, the document is considered readonly. * When not provided, the document is considered readonly.
*/ */
readonly editingDelegate?: CustomEditorEditingDelegate; readonly editingDelegate?: CustomEditorEditingDelegate<EditType>;
} }
/** /**
@@ -1496,6 +1525,7 @@ declare module 'vscode' {
* For binary files or more specialized use cases, see [CustomEditorProvider](#CustomEditorProvider). * For binary files or more specialized use cases, see [CustomEditorProvider](#CustomEditorProvider).
*/ */
export interface CustomTextEditorProvider { export interface CustomTextEditorProvider {
/** /**
* Resolve a webview editor for a given text resource. * Resolve a webview editor for a given text resource.
* *
@@ -1529,8 +1559,6 @@ declare module 'vscode' {
* @return Thenable indicating that the webview editor has been moved. * @return Thenable indicating that the webview editor has been moved.
*/ */
moveCustomTextEditor?(newDocument: TextDocument, existingWebviewPanel: WebviewPanel, token: CancellationToken): Thenable<void>; moveCustomTextEditor?(newDocument: TextDocument, existingWebviewPanel: WebviewPanel, token: CancellationToken): Thenable<void>;
// TODO: handlesMove?: boolean;
} }
namespace window { namespace window {
@@ -1540,14 +1568,16 @@ declare module 'vscode' {
* @param viewType Type of the webview editor provider. This should match the `viewType` from the * @param viewType Type of the webview editor provider. This should match the `viewType` from the
* `package.json` contributions. * `package.json` contributions.
* @param provider Provider that resolves editors. * @param provider Provider that resolves editors.
* @param webviewOptions Content settings for the webview panels that the provider is given. * @param options Options for the provider
* *
* @return Disposable that unregisters the provider. * @return Disposable that unregisters the provider.
*/ */
export function registerCustomEditorProvider( export function registerCustomEditorProvider(
viewType: string, viewType: string,
provider: CustomEditorProvider | CustomTextEditorProvider, provider: CustomEditorProvider | CustomTextEditorProvider,
webviewOptions?: WebviewPanelOptions, // TODO: move this onto provider? options?: {
readonly webviewOptions?: WebviewPanelOptions;
}
): Disposable; ): Disposable;
} }
@@ -1636,7 +1666,16 @@ declare module 'vscode' {
export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput;
export interface NotebookCellMetadata { export interface NotebookCellMetadata {
/**
* Controls if the content of a cell is editable or not.
*/
editable: boolean; editable: boolean;
/**
* Controls if the cell is executable.
* This metadata is ignored for markdown cell.
*/
runnable: boolean;
} }
export interface NotebookCell { export interface NotebookCell {
@@ -1650,7 +1689,23 @@ declare module 'vscode' {
} }
export interface NotebookDocumentMetadata { export interface NotebookDocumentMetadata {
/**
* Controls if users can add or delete cells
* Default to true
*/
editable: boolean; editable: boolean;
/**
* Default value for [cell editable metadata](#NotebookCellMetadata.editable).
* Default to true.
*/
cellEditable: boolean;
/**
* Default value for [cell runnable metadata](#NotebookCellMetadata.runnable).
* Default to true.
*/
cellRunnable: boolean;
} }
export interface NotebookDocument { export interface NotebookDocument {
@@ -1682,7 +1737,7 @@ declare module 'vscode' {
/** /**
* Create a notebook cell. The cell is not inserted into current document when created. Extensions should insert the cell into the document by [TextDocument.cells](#TextDocument.cells) * Create a notebook cell. The cell is not inserted into current document when created. Extensions should insert the cell into the document by [TextDocument.cells](#TextDocument.cells)
*/ */
createCell(content: string, language: string, type: CellKind, outputs: CellOutput[], metadata: NotebookCellMetadata): NotebookCell; createCell(content: string, language: string, type: CellKind, outputs: CellOutput[], metadata: NotebookCellMetadata | undefined): NotebookCell;
} }
export interface NotebookProvider { export interface NotebookProvider {
@@ -1874,17 +1929,10 @@ declare module 'vscode' {
export interface Timeline { export interface Timeline {
readonly paging?: { readonly paging?: {
/** /**
* A set of provider-defined cursors specifing the range of timeline items returned. * A provider-defined cursor specifying the starting point of timeline items which are after the ones returned.
* Use `undefined` to signal that there are no more items to be returned.
*/ */
readonly cursors: { readonly cursor: string | undefined;
readonly before: string;
readonly after?: string
};
/**
* A flag which indicates whether there are more items that weren't returned.
*/
readonly more?: boolean;
} }
/** /**
@@ -1895,19 +1943,15 @@ declare module 'vscode' {
export interface TimelineOptions { export interface TimelineOptions {
/** /**
* A provider-defined cursor specifing the range of timeline items that should be returned. * A provider-defined cursor specifying the starting point of the timeline items that should be returned.
*/ */
cursor?: string; cursor?: string;
/** /**
* A flag to specify whether the timeline items being requested should be before or after (default) the provided cursor. * An optional maximum number timeline items or the all timeline items newer (inclusive) than the timestamp or id that should be returned.
* If `undefined` all timeline items should be returned.
*/ */
before?: boolean; limit?: number | { timestamp: number; id?: string };
/**
* The maximum number or the ending cursor of timeline items that should be returned.
*/
limit?: number | { cursor: string };
} }
export interface TimelineProvider { export interface TimelineProvider {

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import * as modes from 'vs/editor/common/modes'; import * as modes from 'vs/editor/common/modes';
import * as nls from 'vs/nls'; import * as nls from 'vs/nls';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
@@ -12,13 +12,141 @@ import { ExtHostAuthenticationShape, ExtHostContext, IExtHostContext, MainContex
import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import Severity from 'vs/base/common/severity'; import Severity from 'vs/base/common/severity';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
interface AuthDependent {
providerId: string;
label: string;
scopes: string[];
scopeDescriptions?: string;
}
const BUILT_IN_AUTH_DEPENDENTS: AuthDependent[] = [
{
providerId: 'microsoft',
label: 'Settings sync',
scopes: ['https://management.core.windows.net/.default', 'offline_access'],
scopeDescriptions: 'Read user email'
}
];
export class MainThreadAuthenticationProvider extends Disposable {
private _sessionMenuItems = new Map<string, IDisposable[]>();
private _sessionIds: string[] = [];
export class MainThreadAuthenticationProvider {
constructor( constructor(
private readonly _proxy: ExtHostAuthenticationShape, private readonly _proxy: ExtHostAuthenticationShape,
public readonly id: string, public readonly id: string,
public readonly displayName: string public readonly displayName: string,
) { } public readonly dependents: AuthDependent[]
) {
super();
if (!dependents.length) {
return;
}
this.registerCommandsAndContextMenuItems();
}
private setPermissionsForAccount(quickInputService: IQuickInputService, doLogin?: boolean) {
const quickPick = quickInputService.createQuickPick();
quickPick.canSelectMany = true;
const items = this.dependents.map(dependent => {
return {
label: dependent.label,
description: dependent.scopeDescriptions,
picked: true,
scopes: dependent.scopes
};
});
quickPick.items = items;
// TODO read from storage and filter is not doLogin
quickPick.selectedItems = items;
quickPick.title = nls.localize('signInTo', "Sign in to {0}", this.displayName);
quickPick.placeholder = nls.localize('accountPermissions', "Choose what features and extensions to authorize to use this account");
quickPick.onDidAccept(() => {
const scopes = quickPick.selectedItems.reduce((previous, current) => previous.concat((current as any).scopes), []);
if (scopes.length && doLogin) {
this.login(scopes);
}
quickPick.dispose();
});
quickPick.onDidHide(() => {
quickPick.dispose();
});
quickPick.show();
}
private registerCommandsAndContextMenuItems(): void {
this._register(CommandsRegistry.registerCommand({
id: `signIn${this.id}`,
handler: (accessor, args) => {
this.setPermissionsForAccount(accessor.get(IQuickInputService), true);
},
}));
this._register(MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
group: '2_providers',
command: {
id: `signIn${this.id}`,
title: nls.localize('addAccount', "Sign in to {0}", this.displayName)
},
order: 3
}));
this._proxy.$getSessions(this.id).then(sessions => {
sessions.forEach(session => this.registerSession(session));
});
}
private registerSession(session: modes.AuthenticationSession) {
this._sessionIds.push(session.id);
const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
group: '1_accounts',
command: {
id: `configureSessions${session.id}`,
title: session.accountName
},
order: 3
});
const manageCommand = CommandsRegistry.registerCommand({
id: `configureSessions${session.id}`,
handler: (accessor, args) => {
const quickInputService = accessor.get(IQuickInputService);
const quickPick = quickInputService.createQuickPick();
const items = [{ label: 'Sign Out' }];
quickPick.items = items;
quickPick.onDidAccept(e => {
const selected = quickPick.selectedItems[0];
if (selected.label === 'Sign Out') {
this.logout(session.id);
}
quickPick.dispose();
});
quickPick.onDidHide(_ => {
quickPick.dispose();
});
quickPick.show();
},
});
this._sessionMenuItems.set(session.id, [menuItem, manageCommand]);
}
async getSessions(): Promise<ReadonlyArray<modes.AuthenticationSession>> { async getSessions(): Promise<ReadonlyArray<modes.AuthenticationSession>> {
return (await this._proxy.$getSessions(this.id)).map(session => { return (await this._proxy.$getSessions(this.id)).map(session => {
@@ -30,6 +158,24 @@ export class MainThreadAuthenticationProvider {
}); });
} }
async updateSessionItems(): Promise<void> {
const currentSessions = await this._proxy.$getSessions(this.id);
const removedSessionIds = this._sessionIds.filter(id => !currentSessions.some(session => session.id === id));
const addedSessions = currentSessions.filter(session => !this._sessionIds.some(id => id === session.id));
removedSessionIds.forEach(id => {
const disposeables = this._sessionMenuItems.get(id);
if (disposeables) {
disposeables.forEach(disposeable => disposeable.dispose());
this._sessionMenuItems.delete(id);
}
});
addedSessions.forEach(session => this.registerSession(session));
this._sessionIds = currentSessions.map(session => session.id);
}
login(scopes: string[]): Promise<modes.AuthenticationSession> { login(scopes: string[]): Promise<modes.AuthenticationSession> {
return this._proxy.$login(this.id, scopes).then(session => { return this._proxy.$login(this.id, scopes).then(session => {
return { return {
@@ -40,8 +186,14 @@ export class MainThreadAuthenticationProvider {
}); });
} }
logout(accountId: string): Promise<void> { logout(sessionId: string): Promise<void> {
return this._proxy.$logout(this.id, accountId); return this._proxy.$logout(this.id, sessionId);
}
dispose(): void {
super.dispose();
this._sessionMenuItems.forEach(item => item.forEach(d => d.dispose()));
this._sessionMenuItems.clear();
} }
} }
@@ -59,8 +211,10 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication);
} }
$registerAuthenticationProvider(id: string, displayName: string): void { async $registerAuthenticationProvider(id: string, displayName: string): Promise<void> {
const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName); const dependentBuiltIns = BUILT_IN_AUTH_DEPENDENTS.filter(dependency => dependency.providerId === id);
const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, dependentBuiltIns);
this.authenticationService.registerAuthenticationProvider(id, provider); this.authenticationService.registerAuthenticationProvider(id, provider);
} }
@@ -68,8 +222,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
this.authenticationService.unregisterAuthenticationProvider(id); this.authenticationService.unregisterAuthenticationProvider(id);
} }
$onDidChangeSessions(id: string): void { $onDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void {
this.authenticationService.sessionsUpdate(id); this.authenticationService.sessionsUpdate(id, event);
} }
async $getSessionsPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> { async $getSessionsPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {

View File

@@ -8,7 +8,7 @@ import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IEx
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri'; import { URI, UriComponents } from 'vs/base/common/uri';
import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService';
import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellsSplice, NotebookCellOutputsSplice, CellKind, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellsSplice, NotebookCellOutputsSplice, CellKind, NotebookDocumentMetadata, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
@@ -127,7 +127,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
} }
} }
async $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata | undefined): Promise<void> { async $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise<void> {
let controller = this._notebookProviders.get(viewType); let controller = this._notebookProviders.get(viewType);
if (controller) { if (controller) {
@@ -135,6 +135,14 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
} }
} }
async $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata): Promise<void> {
let controller = this._notebookProviders.get(viewType);
if (controller) {
controller.updateNotebookCellMetadata(resource, handle, metadata);
}
}
async resolveNotebook(viewType: string, uri: URI): Promise<number | undefined> { async resolveNotebook(viewType: string, uri: URI): Promise<number | undefined> {
let handle = await this._proxy.$resolveNotebook(viewType, uri); let handle = await this._proxy.$resolveNotebook(viewType, uri);
return handle; return handle;
@@ -228,25 +236,25 @@ export class MainThreadNotebookController implements IMainNotebookController {
document?.textModel.updateLanguages(languages); document?.textModel.updateLanguages(languages);
} }
updateNotebookMetadata(resource: UriComponents, metadata: NotebookDocumentMetadata | undefined) { updateNotebookMetadata(resource: UriComponents, metadata: NotebookDocumentMetadata) {
let document = this._mapping.get(URI.from(resource).toString()); let document = this._mapping.get(URI.from(resource).toString());
document?.textModel.updateNotebookMetadata(metadata); document?.textModel.updateNotebookMetadata(metadata);
} }
updateNotebookCellMetadata(resource: UriComponents, handle: number, metadata: NotebookCellMetadata) {
let document = this._mapping.get(URI.from(resource).toString());
document?.textModel.updateNotebookCellMetadata(handle, metadata);
}
updateNotebookRenderers(resource: UriComponents, renderers: number[]): void { updateNotebookRenderers(resource: UriComponents, renderers: number[]): void {
let document = this._mapping.get(URI.from(resource).toString()); let document = this._mapping.get(URI.from(resource).toString());
document?.textModel.updateRenderers(renderers); document?.textModel.updateRenderers(renderers);
} }
updateNotebookActiveCell(uri: URI, cellHandle: number): void {
let mainthreadNotebook = this._mapping.get(URI.from(uri).toString());
mainthreadNotebook?.textModel.updateActiveCell(cellHandle);
}
async createRawCell(uri: URI, index: number, language: string, type: CellKind): Promise<NotebookCellTextModel | undefined> { async createRawCell(uri: URI, index: number, language: string, type: CellKind): Promise<NotebookCellTextModel | undefined> {
let cell = await this._proxy.$createEmptyCell(this._viewType, uri, index, language, type); let cell = await this._proxy.$createEmptyCell(this._viewType, uri, index, language, type);
if (cell) { if (cell) {
let mainCell = new NotebookCellTextModel(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs); let mainCell = new NotebookCellTextModel(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs, cell.metadata);
return mainCell; return mainCell;
} }
@@ -263,12 +271,8 @@ export class MainThreadNotebookController implements IMainNotebookController {
return false; return false;
} }
async executeNotebookActiveCell(uri: URI): Promise<void> { async executeNotebookCell(uri: URI, handle: number): Promise<void> {
let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); return this._proxy.$executeNotebook(this._viewType, uri, handle);
if (mainthreadNotebook && mainthreadNotebook.textModel.activeCell) {
return this._proxy.$executeNotebook(this._viewType, uri, mainthreadNotebook.textModel.activeCell.handle);
}
} }
async destoryNotebookDocument(notebook: INotebookTextModel): Promise<void> { async destoryNotebookDocument(notebook: INotebookTextModel): Promise<void> {

View File

@@ -11,6 +11,7 @@ import { Disposable, DisposableStore, dispose, IDisposable, IReference } from 'v
import { Schemas } from 'vs/base/common/network'; import { Schemas } from 'vs/base/common/network';
import { basename } from 'vs/base/common/path'; import { basename } from 'vs/base/common/path';
import { isWeb } from 'vs/base/common/platform'; import { isWeb } from 'vs/base/common/platform';
import { isEqual } from 'vs/base/common/resources';
import { escape } from 'vs/base/common/strings'; import { escape } from 'vs/base/common/strings';
import { URI, UriComponents } from 'vs/base/common/uri'; import { URI, UriComponents } from 'vs/base/common/uri';
import * as modes from 'vs/editor/common/modes'; import * as modes from 'vs/editor/common/modes';
@@ -28,6 +29,7 @@ import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } fr
import { IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput';
import { CustomDocumentBackupData } from 'vs/workbench/contrib/customEditor/browser/customEditorInputFactory';
import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor';
import { CustomTextEditorModel } from 'vs/workbench/contrib/customEditor/common/customTextEditorModel'; import { CustomTextEditorModel } from 'vs/workbench/contrib/customEditor/common/customTextEditorModel';
import { WebviewExtensionDescription, WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewExtensionDescription, WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview';
@@ -70,6 +72,10 @@ class WebviewInputStore {
public get size(): number { public get size(): number {
return this._handlesToInputs.size; return this._handlesToInputs.size;
} }
[Symbol.iterator](): Iterator<WebviewInput> {
return this._handlesToInputs.values();
}
} }
class WebviewViewTypeTransformer { class WebviewViewTypeTransformer {
@@ -374,7 +380,10 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
const model = modelType === ModelType.Text const model = modelType === ModelType.Text
? CustomTextEditorModel.create(this._instantiationService, viewType, resource) ? CustomTextEditorModel.create(this._instantiationService, viewType, resource)
: MainThreadCustomEditorModel.create(this._instantiationService, this._proxy, viewType, resource, cancellation); : MainThreadCustomEditorModel.create(this._instantiationService, this._proxy, viewType, resource, () => {
return Array.from(this._webviewInputs)
.filter(editor => editor instanceof CustomEditorInput && isEqual(editor.resource, resource)) as CustomEditorInput[];
}, cancellation);
return this._customEditorService.models.add(resource, viewType, model); return this._customEditorService.models.add(resource, viewType, model);
} }
@@ -548,7 +557,6 @@ namespace HotExitState {
export type State = typeof Allowed | typeof NotAllowed | Pending; export type State = typeof Allowed | typeof NotAllowed | Pending;
} }
const customDocumentFileScheme = 'custom';
class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy { class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy {
@@ -562,17 +570,19 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
proxy: extHostProtocol.ExtHostWebviewsShape, proxy: extHostProtocol.ExtHostWebviewsShape,
viewType: string, viewType: string,
resource: URI, resource: URI,
getEditors: () => CustomEditorInput[],
cancellation: CancellationToken, cancellation: CancellationToken,
) { ) {
const { editable } = await proxy.$createWebviewCustomEditorDocument(resource, viewType, cancellation); const { editable } = await proxy.$createWebviewCustomEditorDocument(resource, viewType, cancellation);
return instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, editable); return instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, editable, getEditors);
} }
constructor( constructor(
private readonly _proxy: extHostProtocol.ExtHostWebviewsShape, private readonly _proxy: extHostProtocol.ExtHostWebviewsShape,
private readonly _viewType: string, private readonly _viewType: string,
private readonly _realResource: URI, private readonly _editorResource: URI,
private readonly _editable: boolean, private readonly _editable: boolean,
private readonly _getEditors: () => CustomEditorInput[],
@IWorkingCopyService workingCopyService: IWorkingCopyService, @IWorkingCopyService workingCopyService: IWorkingCopyService,
@ILabelService private readonly _labelService: ILabelService, @ILabelService private readonly _labelService: ILabelService,
@IFileService private readonly _fileService: IFileService, @IFileService private readonly _fileService: IFileService,
@@ -587,9 +597,9 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
dispose() { dispose() {
if (this._editable) { if (this._editable) {
this._undoService.removeElements(this._realResource); this._undoService.removeElements(this._editorResource);
} }
this._proxy.$disposeWebviewCustomEditorDocument(this._realResource, this._viewType); this._proxy.$disposeWebviewCustomEditorDocument(this._editorResource, this._viewType);
super.dispose(); super.dispose();
} }
@@ -598,15 +608,15 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
public get resource() { public get resource() {
// Make sure each custom editor has a unique resource for backup and edits // Make sure each custom editor has a unique resource for backup and edits
return URI.from({ return URI.from({
scheme: customDocumentFileScheme, scheme: Schemas.vscodeCustomEditor,
authority: this._viewType, authority: this._viewType,
path: this._realResource.path, path: this._editorResource.path,
query: JSON.stringify(this._realResource.toJSON()) query: JSON.stringify(this._editorResource.toJSON()),
}); });
} }
public get name() { public get name() {
return basename(this._labelService.getUriLabel(this._realResource)); return basename(this._labelService.getUriLabel(this._editorResource));
} }
public get capabilities(): WorkingCopyCapabilities { public get capabilities(): WorkingCopyCapabilities {
@@ -645,7 +655,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
this._undoService.pushElement({ this._undoService.pushElement({
type: UndoRedoElementType.Resource, type: UndoRedoElementType.Resource,
resource: this._realResource, resource: this._editorResource,
label: label ?? localize('defaultEditLabel', "Edit"), label: label ?? localize('defaultEditLabel', "Edit"),
undo: () => this.undo(), undo: () => this.undo(),
redo: () => this.redo(), redo: () => this.redo(),
@@ -663,11 +673,18 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
} }
const undoneEdit = this._edits[this._currentEditIndex]; const undoneEdit = this._edits[this._currentEditIndex];
await this._proxy.$undo(this._realResource, this.viewType, undoneEdit);
this.change(() => { this.change(() => {
--this._currentEditIndex; --this._currentEditIndex;
}); });
await this._proxy.$undo(this._editorResource, this.viewType, undoneEdit, this.getEditState());
}
private getEditState(): extHostProtocol.CustomDocumentEditState {
return {
allEdits: this._edits,
currentIndex: this._currentEditIndex,
saveIndex: this._savePoint,
};
} }
private async redo(): Promise<void> { private async redo(): Promise<void> {
@@ -681,10 +698,10 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
} }
const redoneEdit = this._edits[this._currentEditIndex + 1]; const redoneEdit = this._edits[this._currentEditIndex + 1];
await this._proxy.$redo(this._realResource, this.viewType, redoneEdit);
this.change(() => { this.change(() => {
++this._currentEditIndex; ++this._currentEditIndex;
}); });
await this._proxy.$redo(this._editorResource, this.viewType, redoneEdit, this.getEditState());
} }
private spliceEdits(editToInsert?: number) { private spliceEdits(editToInsert?: number) {
@@ -696,7 +713,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
: this._edits.splice(start, toRemove); : this._edits.splice(start, toRemove);
if (removedEdits.length) { if (removedEdits.length) {
this._proxy.$disposeEdits(this._realResource, this._viewType, removedEdits); this._proxy.$disposeEdits(this._editorResource, this._viewType, removedEdits);
} }
} }
@@ -728,7 +745,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
editsToRedo = this._edits.slice(this._currentEditIndex, this._savePoint); editsToRedo = this._edits.slice(this._currentEditIndex, this._savePoint);
} }
this._proxy.$revert(this._realResource, this.viewType, { undoneEdits: editsToUndo, redoneEdits: editsToRedo }); this._proxy.$revert(this._editorResource, this.viewType, { undoneEdits: editsToUndo, redoneEdits: editsToRedo }, this.getEditState());
this.change(() => { this.change(() => {
this._currentEditIndex = this._savePoint; this._currentEditIndex = this._savePoint;
this.spliceEdits(); this.spliceEdits();
@@ -739,7 +756,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
if (!this._editable) { if (!this._editable) {
return false; return false;
} }
await createCancelablePromise(token => this._proxy.$onSave(this._realResource, this.viewType, token)); await createCancelablePromise(token => this._proxy.$onSave(this._editorResource, this.viewType, token));
this.change(() => { this.change(() => {
this._savePoint = this._currentEditIndex; this._savePoint = this._currentEditIndex;
}); });
@@ -748,7 +765,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
public async saveAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise<boolean> { public async saveAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise<boolean> {
if (this._editable) { if (this._editable) {
await this._proxy.$onSaveAs(this._realResource, this.viewType, targetResource); await this._proxy.$onSaveAs(this._editorResource, this.viewType, targetResource);
this.change(() => { this.change(() => {
this._savePoint = this._currentEditIndex; this._savePoint = this._currentEditIndex;
}); });
@@ -761,9 +778,25 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
} }
public async backup(): Promise<IWorkingCopyBackup> { public async backup(): Promise<IWorkingCopyBackup> {
const backupData: IWorkingCopyBackup = { const editors = this._getEditors();
if (!editors.length) {
throw new Error('No editors found for resource, cannot back up');
}
const primaryEditor = editors[0];
const backupData: IWorkingCopyBackup<CustomDocumentBackupData> = {
meta: { meta: {
viewType: this.viewType, viewType: this.viewType,
editorResource: this._editorResource,
extension: primaryEditor.extension ? {
id: primaryEditor.extension.id.value,
location: primaryEditor.extension.location,
} : undefined,
webview: {
id: primaryEditor.id,
options: primaryEditor.webview.options,
state: primaryEditor.webview.state,
}
} }
}; };
@@ -777,7 +810,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
const pendingState = new HotExitState.Pending( const pendingState = new HotExitState.Pending(
createCancelablePromise(token => createCancelablePromise(token =>
this._proxy.$backup(this._realResource.toJSON(), this.viewType, token))); this._proxy.$backup(this._editorResource.toJSON(), this.viewType, token)));
this._hotExitState = pendingState; this._hotExitState = pendingState;
try { try {

View File

@@ -202,7 +202,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
login(providerId: string, scopes: string[]): Thenable<vscode.AuthenticationSession> { login(providerId: string, scopes: string[]): Thenable<vscode.AuthenticationSession> {
return extHostAuthentication.login(extension, providerId, scopes); return extHostAuthentication.login(extension, providerId, scopes);
}, },
get onDidChangeSessions(): Event<string[]> { get onDidChangeSessions(): Event<{ [providerId: string]: vscode.AuthenticationSessionsChangeEvent }> {
return extHostAuthentication.onDidChangeSessions; return extHostAuthentication.onDidChangeSessions;
}, },
}; };
@@ -549,9 +549,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
return extHostProgress.withProgress(extension, { location: extHostTypes.ProgressLocation.SourceControl }, (progress, token) => task({ report(n: number) { /*noop*/ } })); return extHostProgress.withProgress(extension, { location: extHostTypes.ProgressLocation.SourceControl }, (progress, token) => task({ report(n: number) { /*noop*/ } }));
}, },
withProgress<R>(options: vscode.ProgressOptions, task: (progress: vscode.Progress<{ message?: string; worked?: number }>, token: vscode.CancellationToken) => Thenable<R>) { withProgress<R>(options: vscode.ProgressOptions, task: (progress: vscode.Progress<{ message?: string; worked?: number }>, token: vscode.CancellationToken) => Thenable<R>) {
if (typeof options.location === 'object') {
checkProposedApiEnabled(extension);
}
return extHostProgress.withProgress(extension, options, task); return extHostProgress.withProgress(extension, options, task);
}, },
createOutputChannel(name: string): vscode.OutputChannel { createOutputChannel(name: string): vscode.OutputChannel {
@@ -586,9 +583,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => { registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => {
return extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializer); return extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializer);
}, },
registerCustomEditorProvider: (viewType: string, provider: vscode.CustomEditorProvider | vscode.CustomTextEditorProvider, options?: vscode.WebviewPanelOptions) => { registerCustomEditorProvider: (viewType: string, provider: vscode.CustomEditorProvider | vscode.CustomTextEditorProvider, options?: { webviewOptions?: vscode.WebviewPanelOptions }) => {
checkProposedApiEnabled(extension); checkProposedApiEnabled(extension);
return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options); return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options?.webviewOptions);
}, },
registerDecorationProvider(provider: vscode.DecorationProvider) { registerDecorationProvider(provider: vscode.DecorationProvider) {
checkProposedApiEnabled(extension); checkProposedApiEnabled(extension);
@@ -1048,12 +1045,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
CallHierarchyItem: extHostTypes.CallHierarchyItem, CallHierarchyItem: extHostTypes.CallHierarchyItem,
DebugConsoleMode: extHostTypes.DebugConsoleMode, DebugConsoleMode: extHostTypes.DebugConsoleMode,
Decoration: extHostTypes.Decoration, Decoration: extHostTypes.Decoration,
WebviewContentState: extHostTypes.WebviewContentState,
UIKind: UIKind, UIKind: UIKind,
ColorThemeKind: extHostTypes.ColorThemeKind, ColorThemeKind: extHostTypes.ColorThemeKind,
TimelineItem: extHostTypes.TimelineItem, TimelineItem: extHostTypes.TimelineItem,
CellKind: extHostTypes.CellKind, CellKind: extHostTypes.CellKind,
CellOutputKind: extHostTypes.CellOutputKind CellOutputKind: extHostTypes.CellOutputKind,
CustomDocument: extHostTypes.CustomDocument,
}; };
}; };
} }

View File

@@ -160,7 +160,7 @@ export interface MainThreadCommentsShape extends IDisposable {
export interface MainThreadAuthenticationShape extends IDisposable { export interface MainThreadAuthenticationShape extends IDisposable {
$registerAuthenticationProvider(id: string, displayName: string): void; $registerAuthenticationProvider(id: string, displayName: string): void;
$unregisterAuthenticationProvider(id: string): void; $unregisterAuthenticationProvider(id: string): void;
$onDidChangeSessions(id: string): void; $onDidChangeSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent): void;
$getSessionsPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean>; $getSessionsPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean>;
$loginPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean>; $loginPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean>;
} }
@@ -626,6 +626,12 @@ export interface WebviewPanelViewStateData {
}; };
} }
export interface CustomDocumentEditState {
readonly allEdits: readonly number[];
readonly currentIndex: number;
readonly saveIndex: number;
}
export interface ExtHostWebviewsShape { export interface ExtHostWebviewsShape {
$onMessage(handle: WebviewPanelHandle, message: any): void; $onMessage(handle: WebviewPanelHandle, message: any): void;
$onMissingCsp(handle: WebviewPanelHandle, extensionId: string): void; $onMissingCsp(handle: WebviewPanelHandle, extensionId: string): void;
@@ -638,9 +644,9 @@ export interface ExtHostWebviewsShape {
$createWebviewCustomEditorDocument(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<{ editable: boolean }>; $createWebviewCustomEditorDocument(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<{ editable: boolean }>;
$disposeWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise<void>; $disposeWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise<void>;
$undo(resource: UriComponents, viewType: string, editId: number): Promise<void>; $undo(resource: UriComponents, viewType: string, editId: number, state: CustomDocumentEditState): Promise<void>;
$redo(resource: UriComponents, viewType: string, editId: number): Promise<void>; $redo(resource: UriComponents, viewType: string, editId: number, state: CustomDocumentEditState): Promise<void>;
$revert(resource: UriComponents, viewType: string, changes: { undoneEdits: number[], redoneEdits: number[] }): Promise<void>; $revert(resource: UriComponents, viewType: string, changes: { undoneEdits: number[], redoneEdits: number[] }, state: CustomDocumentEditState): Promise<void>;
$disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void; $disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void;
$onSave(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void>; $onSave(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void>;
@@ -691,7 +697,8 @@ export interface MainThreadNotebookShape extends IDisposable {
$unregisterNotebookRenderer(handle: number): Promise<void>; $unregisterNotebookRenderer(handle: number): Promise<void>;
$createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void>; $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void>;
$updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise<void>; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise<void>;
$updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata | undefined): Promise<void>; $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise<void>;
$updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata | undefined): Promise<void>;
$spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): Promise<void>; $spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): Promise<void>;
$spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise<void>; $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise<void>;
$postMessage(handle: number, value: any): Promise<boolean>; $postMessage(handle: number, value: any): Promise<boolean>;

View File

@@ -17,8 +17,8 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
private _onDidChangeAuthenticationProviders = new Emitter<vscode.AuthenticationProvidersChangeEvent>(); private _onDidChangeAuthenticationProviders = new Emitter<vscode.AuthenticationProvidersChangeEvent>();
readonly onDidChangeAuthenticationProviders: Event<vscode.AuthenticationProvidersChangeEvent> = this._onDidChangeAuthenticationProviders.event; readonly onDidChangeAuthenticationProviders: Event<vscode.AuthenticationProvidersChangeEvent> = this._onDidChangeAuthenticationProviders.event;
private _onDidChangeSessions = new Emitter<string[]>(); private _onDidChangeSessions = new Emitter<{ [providerId: string]: vscode.AuthenticationSessionsChangeEvent }>();
readonly onDidChangeSessions: Event<string[]> = this._onDidChangeSessions.event; readonly onDidChangeSessions: Event<{ [providerId: string]: vscode.AuthenticationSessionsChangeEvent }> = this._onDidChangeSessions.event;
constructor(mainContext: IMainContext) { constructor(mainContext: IMainContext) {
this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication); this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication);
@@ -85,9 +85,9 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
this._authenticationProviders.set(provider.id, provider); this._authenticationProviders.set(provider.id, provider);
const listener = provider.onDidChangeSessions(_ => { const listener = provider.onDidChangeSessions(e => {
this._proxy.$onDidChangeSessions(provider.id); this._proxy.$onDidChangeSessions(provider.id, e);
this._onDidChangeSessions.fire([provider.id]); this._onDidChangeSessions.fire({ [provider.id]: e });
}); });
this._proxy.$registerAuthenticationProvider(provider.id, provider.displayName); this._proxy.$registerAuthenticationProvider(provider.id, provider.displayName);

View File

@@ -16,6 +16,12 @@ import { INotebookDisplayOrder, ITransformedDisplayOutputDto, IOrderedMimeType,
import { ISplice } from 'vs/base/common/sequence'; import { ISplice } from 'vs/base/common/sequence';
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
const notebookDocumentMetadataDefaults: vscode.NotebookDocumentMetadata = {
editable: true,
cellEditable: true,
cellRunnable: true
};
export class ExtHostCell implements vscode.NotebookCell { export class ExtHostCell implements vscode.NotebookCell {
public source: string[]; public source: string[];
@@ -27,13 +33,16 @@ export class ExtHostCell implements vscode.NotebookCell {
private _outputMapping = new Set<vscode.CellOutput>(); private _outputMapping = new Set<vscode.CellOutput>();
constructor( constructor(
private viewType: string,
private documentUri: URI,
readonly handle: number, readonly handle: number,
readonly uri: URI, readonly uri: URI,
private _content: string, private _content: string,
public cellKind: CellKind, public cellKind: CellKind,
public language: string, public language: string,
outputs: any[], outputs: any[],
public metadata: vscode.NotebookCellMetadata | undefined, private _metadata: vscode.NotebookCellMetadata | undefined,
private _proxy: MainThreadNotebookShape
) { ) {
this.source = this._content.split(/\r|\n|\r\n/g); this.source = this._content.split(/\r|\n|\r\n/g);
this._outputs = outputs; this._outputs = outputs;
@@ -62,6 +71,20 @@ export class ExtHostCell implements vscode.NotebookCell {
this._onDidChangeOutputs.fire(diffs); this._onDidChangeOutputs.fire(diffs);
} }
get metadata() {
return this._metadata;
}
set metadata(newMetadata: vscode.NotebookCellMetadata | undefined) {
const newMetadataWithDefaults: vscode.NotebookCellMetadata | undefined = newMetadata ? {
editable: newMetadata.editable,
runnable: newMetadata.runnable
} : undefined;
this._metadata = newMetadataWithDefaults;
this._proxy.$updateNotebookCellMetadata(this.viewType, this.documentUri, this.handle, newMetadataWithDefaults);
}
getContent(): string { getContent(): string {
if (this._textDocument && this._initalVersion !== this._textDocument?.version) { if (this._textDocument && this._initalVersion !== this._textDocument?.version) {
return this._textDocument.getText(); return this._textDocument.getText();
@@ -131,14 +154,14 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo
this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages); this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages);
} }
private _metadata: vscode.NotebookDocumentMetadata | undefined = undefined; private _metadata: vscode.NotebookDocumentMetadata | undefined = notebookDocumentMetadataDefaults;
get metadata() { get metadata() {
return this._metadata; return this._metadata;
} }
set metadata(newMetadata: vscode.NotebookDocumentMetadata | undefined) { set metadata(newMetadata: vscode.NotebookDocumentMetadata | undefined) {
this._metadata = newMetadata; this._metadata = newMetadata || notebookDocumentMetadataDefaults;
this._proxy.$updateNotebookMetadata(this.viewType, this.uri, this._metadata); this._proxy.$updateNotebookMetadata(this.viewType, this.uri, this._metadata);
} }
@@ -201,6 +224,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo
language: cell.language, language: cell.language,
cellKind: cell.cellKind, cellKind: cell.cellKind,
outputs: outputs, outputs: outputs,
metadata: cell.metadata,
isDirty: false isDirty: false
}; };
}); });
@@ -346,7 +370,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook
onDidReceiveMessage: vscode.Event<any> = this._onDidReceiveMessage.event; onDidReceiveMessage: vscode.Event<any> = this._onDidReceiveMessage.event;
constructor( constructor(
viewType: string, private readonly viewType: string,
readonly id: string, readonly id: string,
public uri: URI, public uri: URI,
private _proxy: MainThreadNotebookShape, private _proxy: MainThreadNotebookShape,
@@ -381,7 +405,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook
createCell(content: string, language: string, type: CellKind, outputs: vscode.CellOutput[], metadata: vscode.NotebookCellMetadata | undefined): vscode.NotebookCell { createCell(content: string, language: string, type: CellKind, outputs: vscode.CellOutput[], metadata: vscode.NotebookCellMetadata | undefined): vscode.NotebookCell {
const handle = ExtHostNotebookEditor._cellhandlePool++; const handle = ExtHostNotebookEditor._cellhandlePool++;
const uri = CellUri.generate(this.document.uri, handle); const uri = CellUri.generate(this.document.uri, handle);
const cell = new ExtHostCell(handle, uri, content, type, language, outputs, metadata); const cell = new ExtHostCell(this.viewType, this.uri, handle, uri, content, type, language, outputs, metadata, this._proxy);
return cell; return cell;
} }
@@ -469,9 +493,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
} }
} }
} }
return arg;
} }
return arg;
} }
}); });
} }
@@ -580,7 +603,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
let editor = this._editors.get(URI.revive(uri).toString()); let editor = this._editors.get(URI.revive(uri).toString());
let document = this._documents.get(URI.revive(uri).toString()); let document = this._documents.get(URI.revive(uri).toString());
let rawCell = editor?.editor.createCell('', language, type, [], undefined) as ExtHostCell; let rawCell = editor?.editor.createCell('', language, type, [], { editable: true, runnable: true }) as ExtHostCell;
document?.insertCell(index, rawCell!); document?.insertCell(index, rawCell!);
let allDocuments = this._documentsAndEditors.allDocuments(); let allDocuments = this._documentsAndEditors.allDocuments();

View File

@@ -4,16 +4,19 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { coalesce, equals } from 'vs/base/common/arrays'; import { coalesce, equals } from 'vs/base/common/arrays';
import { escapeCodicons } from 'vs/base/common/codicons';
import { illegalArgument } from 'vs/base/common/errors'; import { illegalArgument } from 'vs/base/common/errors';
import { Emitter } from 'vs/base/common/event';
import { IRelativePattern } from 'vs/base/common/glob'; import { IRelativePattern } from 'vs/base/common/glob';
import { isMarkdownString } from 'vs/base/common/htmlContent'; import { isMarkdownString } from 'vs/base/common/htmlContent';
import { startsWith } from 'vs/base/common/strings'; import { startsWith } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid'; import { generateUuid } from 'vs/base/common/uuid';
import type * as vscode from 'vscode';
import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files'; import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files';
import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { escapeCodicons } from 'vs/base/common/codicons'; import type * as vscode from 'vscode';
import { Cache } from './cache';
import { assertIsDefined } from 'vs/base/common/types';
function es5ClassCompat(target: Function): any { function es5ClassCompat(target: Function): any {
///@ts-ignore ///@ts-ignore
@@ -2538,13 +2541,6 @@ export class Decoration {
bubble?: boolean; bubble?: boolean;
} }
export enum WebviewContentState {
Readonly = 1,
Unchanged = 2,
Dirty = 3,
}
//#region Theming //#region Theming
@es5ClassCompat @es5ClassCompat
@@ -2584,3 +2580,84 @@ export class TimelineItem implements vscode.TimelineItem {
} }
//#endregion Timeline //#endregion Timeline
//#region Custom Editors
interface EditState {
readonly allEdits: readonly number[];
readonly currentIndex: number;
readonly saveIndex: number;
}
export class CustomDocument<EditType = unknown> implements vscode.CustomDocument<EditType> {
readonly #edits = new Cache<EditType>('edits');
#editState: EditState;
readonly #viewType: string;
readonly #uri: vscode.Uri;
constructor(viewType: string, uri: vscode.Uri) {
this.#viewType = viewType;
this.#uri = uri;
this.#editState = {
allEdits: [],
currentIndex: 0,
saveIndex: 0
};
}
//#region Public API
public get viewType(): string { return this.#viewType; }
public get uri(): vscode.Uri { return this.#uri; }
#onDidDispose = new Emitter<void>();
public readonly onDidDispose = this.#onDidDispose.event;
get appliedEdits() {
return this.#editState.allEdits.slice(0, this.#editState.currentIndex + 1)
.map(id => this._getEdit(id));
}
get savedEdits() {
return this.#editState.allEdits.slice(0, this.#editState.saveIndex + 1)
.map(id => this._getEdit(id));
}
//#endregion
/** @internal */ _dispose(): void {
this.#onDidDispose.fire();
this.#onDidDispose.dispose();
}
/** @internal */ _updateEditState(state: EditState) {
this.#editState = state;
}
/** @internal*/ _getEdit(editId: number): EditType {
return assertIsDefined(this.#edits.get(editId, 0));
}
/** @internal*/ _disposeEdits(editIds: number[]) {
for (const editId of editIds) {
this.#edits.delete(editId);
}
}
/** @internal*/ _addEdit(edit: EditType): number {
const id = this.#edits.add([edit]);
this.#editState = {
allEdits: [...this.#editState.allEdits.slice(0, this.#editState.currentIndex), id],
currentIndex: this.#editState.currentIndex + 1,
saveIndex: this.#editState.saveIndex,
};
return id;
}
}
// #endregion

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