mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-04-12 14:31:48 -04:00
Merge from vscode 073a24de05773f2261f89172987002dc0ae2f1cd (#9711)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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...');
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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; }`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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++) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 @@
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
) { }
|
) { }
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
326
src/vs/editor/common/model/textChange.ts
Normal file
326
src/vs/editor/common/model/textChange.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 [];
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)));
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
269
src/vs/editor/test/common/model/textChange.test.ts
Normal file
269
src/vs/editor/test/common/model/textChange.test.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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
15
src/vs/monaco.d.ts
vendored
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -533,6 +533,7 @@ export class Menubar {
|
|||||||
[
|
[
|
||||||
minimize,
|
minimize,
|
||||||
zoom,
|
zoom,
|
||||||
|
__separator__(),
|
||||||
switchWindow,
|
switchWindow,
|
||||||
...nativeTabMenuItems,
|
...nativeTabMenuItems,
|
||||||
__separator__(),
|
__separator__(),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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' /* ... */;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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] {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
160
src/vs/vscode.proposed.d.ts
vendored
160
src/vs/vscode.proposed.d.ts
vendored
@@ -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 {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user