diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 7e4142b566..4ed93a41f4 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -77,6 +77,23 @@ steps: yarn postinstall displayName: Run postinstall scripts 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: | set -e diff --git a/build/azure-pipelines/linux/product-build-linux-multiarch.yml b/build/azure-pipelines/linux/product-build-linux-multiarch.yml index 68ae4ee8b6..066e42af3d 100644 --- a/build/azure-pipelines/linux/product-build-linux-multiarch.yml +++ b/build/azure-pipelines/linux/product-build-linux-multiarch.yml @@ -86,6 +86,23 @@ steps: yarn postinstall displayName: Run postinstall scripts 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: | set -e diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index cbe3bf051e..119f80cd92 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -76,6 +76,23 @@ steps: yarn postinstall displayName: Run postinstall scripts 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: | set -e diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 4891618932..6c28724824 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -92,6 +92,8 @@ steps: 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) # Mixin must run before optimize, because the CSS loader will # inline small SVGs diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 7a8a12aa28..20699bf922 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -86,6 +86,23 @@ steps: exec { yarn postinstall } displayName: Run postinstall scripts 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: | . build/azure-pipelines/win32/exec.ps1 diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index df81f2c390..8e560b96cb 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -349,6 +349,10 @@ { "name": "vs/workbench/contrib/timeline", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/services/authentication", + "project": "vscode-workbench" } ] } diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index 77f8eb52ce..0f728dfca3 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -23,7 +23,7 @@ if (majorYarnVersion < 1 || minorYarnVersion < 10) { 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'); err = true; } diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 0defd3aee6..f929c260fd 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -96,6 +96,10 @@ "fileMatch": "%APP_SETTINGS_HOME%/snippets/*.json", "url": "vscode://schemas/snippets" }, + { + "fileMatch": "%APP_SETTINGS_HOME%/sync/snippets/preview/*.json", + "url": "vscode://schemas/snippets" + }, { "fileMatch": "**/*.code-snippets", "url": "vscode://schemas/global-snippets" diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 95fb2a01de..feb24055a6 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2360,7 +2360,13 @@ export class CommandCenter { 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 }) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index faa524f805..fb3d0150cd 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -45,7 +45,7 @@ interface MutableRemote extends Remote { 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. */ diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 31fd356bd0..559e889d67 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -15,7 +15,7 @@ dayjs.extend(advancedFormat); 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 { static is(item: TimelineItem): item is GitTimelineItem { @@ -71,21 +71,21 @@ export class GitTimelineProvider implements TimelineProvider { readonly id = 'git-history'; readonly label = localize('git.timeline.source', 'Git History'); - private _disposable: Disposable; + private disposable: Disposable; - private _repo: Repository | undefined; - private _repoDisposable: Disposable | undefined; - private _repoStatusDate: Date | undefined; + private repo: Repository | undefined; + private repoDisposable: Disposable | undefined; + private repoStatusDate: Date | undefined; constructor(private readonly _model: Model) { - this._disposable = Disposable.from( + this.disposable = Disposable.from( _model.onDidOpenRepository(this.onRepositoriesChanged, this), workspace.registerTimelineProvider(['file', 'git', 'gitlens-git'], this), ); } dispose() { - this._disposable.dispose(); + this.disposable.dispose(); } async provideTimeline(uri: Uri, options: TimelineOptions, _token: CancellationToken): Promise { @@ -93,33 +93,33 @@ export class GitTimelineProvider implements TimelineProvider { const repo = this._model.getRepository(uri); if (!repo) { - this._repoDisposable?.dispose(); - this._repoStatusDate = undefined; - this._repo = undefined; + this.repoDisposable?.dispose(); + this.repoStatusDate = undefined; + this.repo = undefined; return { items: [] }; } - if (this._repo?.root !== repo.root) { - this._repoDisposable?.dispose(); + if (this.repo?.root !== repo.root) { + this.repoDisposable?.dispose(); - this._repo = repo; - this._repoStatusDate = new Date(); - this._repoDisposable = Disposable.from( + this.repo = repo; + this.repoStatusDate = new Date(); + this.repoDisposable = Disposable.from( repo.onDidChangeRepository(uri => this.onRepositoryChanged(repo, uri)), 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; if (options.limit !== undefined && typeof options.limit !== 'number') { 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) { - // Ask for 1 more than so we can determine if there are more commits - limit = Number(result.stdout) + 1; + // 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) + 2; } } catch { @@ -130,21 +130,14 @@ export class GitTimelineProvider implements TimelineProvider { limit = options.limit === undefined ? undefined : options.limit + 1; } - const commits = await repo.logFile(uri, { maxEntries: limit, hash: options.cursor, - reverse: options.before, // sortByAuthorDate: true }); - const more = limit === undefined || options.before ? false : commits.length >= limit; const paging = commits.length ? { - more: more, - cursors: { - before: commits[0]?.hash, - after: commits[commits.length - (more ? 1 : 2)]?.hash - } + cursor: limit === undefined ? undefined : (commits.length >= limit ? commits[commits.length - 1]?.hash : undefined) } : undefined; // If we asked for an extra commit, strip it off @@ -153,12 +146,12 @@ export class GitTimelineProvider implements TimelineProvider { } let dateFormatter: dayjs.Dayjs; - const items = commits.map(c => { + const items = commits.map((c, i) => { const date = c.commitDate; // c.authorDate 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.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}`; @@ -171,16 +164,16 @@ export class GitTimelineProvider implements TimelineProvider { return item; }); - if (options.cursor === undefined || options.before) { + if (options.cursor === undefined) { const you = localize('git.timeline.you', 'You'); const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath); if (index) { - const date = this._repoStatusDate ?? new Date(); + const date = this.repoStatusDate ?? new Date(); dateFormatter = dayjs(date); 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.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)); @@ -199,7 +192,7 @@ export class GitTimelineProvider implements TimelineProvider { dateFormatter = dayjs(date); 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.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)); @@ -222,7 +215,7 @@ export class GitTimelineProvider implements TimelineProvider { private onRepositoriesChanged(_repo: Repository) { // 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(); } @@ -236,7 +229,7 @@ export class GitTimelineProvider implements TimelineProvider { // 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._repoStatusDate = new Date(); + this.repoStatusDate = new Date(); this.fireChanged(); } diff --git a/extensions/github-authentication/build/postinstall.js b/extensions/github-authentication/build/postinstall.js index 239783e304..58a5ecbacb 100644 --- a/extensions/github-authentication/build/postinstall.js +++ b/extensions/github-authentication/build/postinstall.js @@ -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(); diff --git a/extensions/github-authentication/src/common/clientRegistrar.ts b/extensions/github-authentication/src/common/clientRegistrar.ts index d5f08c6886..0cae2366e6 100644 --- a/extensions/github-authentication/src/common/clientRegistrar.ts +++ b/extensions/github-authentication/src/common/clientRegistrar.ts @@ -3,7 +3,9 @@ * 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 { id?: string; @@ -19,6 +21,8 @@ export interface ClientConfig { VSO: ClientDetails; VSO_PPE: ClientDetails; VSO_DEV: ClientDetails; + + GITHUB_APP: ClientDetails; } export class Registrar { @@ -26,7 +30,8 @@ export class Registrar { constructor() { 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) { this._config = { OSS: {}, @@ -35,10 +40,20 @@ export class Registrar { EXPLORATION: {}, VSO: {}, 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 { let details: ClientDetails | undefined; switch (callbackUri.scheme) { diff --git a/extensions/github-authentication/src/extension.ts b/extensions/github-authentication/src/extension.ts index c6008efdf8..0ec2c15cbc 100644 --- a/extensions/github-authentication/src/extension.ts +++ b/extensions/github-authentication/src/extension.ts @@ -20,9 +20,9 @@ export async function activate(context: vscode.ExtensionContext) { displayName: 'GitHub', onDidChangeSessions: onDidChangeSessions.event, getSessions: () => Promise.resolve(loginService.sessions), - login: async (scopes: string[]) => { + login: async (scopeList: string[]) => { try { - const session = await loginService.login(scopes.join(' ')); + const session = await loginService.login(scopeList.join(' ')); Logger.info('Login success!'); return session; } catch (e) { diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 4bfa0feff3..bb9a590348 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -8,7 +8,7 @@ import { keychain } from './common/keychain'; import { GitHubServer } from './githubServer'; import Logger from './common/logger'; -export const onDidChangeSessions = new vscode.EventEmitter(); +export const onDidChangeSessions = new vscode.EventEmitter(); interface SessionData { id: string; @@ -29,14 +29,16 @@ export class GitHubAuthenticationProvider { private pollForChange() { setTimeout(async () => { const storedSessions = await this.readSessions(); - let didChange = false; + + const added: string[] = []; + const removed: string[] = []; storedSessions.forEach(session => { 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 if (!matchesExisting) { this._sessions.push(session); - didChange = true; + added.push(session.id); } }); @@ -49,12 +51,12 @@ export class GitHubAuthenticationProvider { this._sessions.splice(sessionIndex, 1); } - didChange = true; + removed.push(session.id); } }); - if (didChange) { - onDidChangeSessions.fire(); + if (added.length || removed.length) { + onDidChangeSessions.fire({ added, removed, changed: [] }); } this.pollForChange(); @@ -101,12 +103,22 @@ export class GitHubAuthenticationProvider { } public async login(scopes: string): Promise { - 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(' ')); await this.setToken(session); return session; } + public async loginAndInstallApp(scopes: string): Promise { + 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 { const userInfo = await this._githubServer.getUserInfo(token); return { diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index f395a1a3e7..efceb637a8 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -71,13 +71,58 @@ export class GitHubServer { Logger.info('Logging in...'); const state = uuid(); 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}`); vscode.env.openExternal(uri); return promiseFromEvent(uriHandler.event, exchangeCodeForToken(state, clientDetails)); } + public async hasUserInstallation(token: string): Promise { + 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 { + 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 }> { return new Promise((resolve, reject) => { Logger.info('Getting account info...'); diff --git a/extensions/image-preview/src/preview.ts b/extensions/image-preview/src/preview.ts index f9971209a9..8a5f30c98e 100644 --- a/extensions/image-preview/src/preview.ts +++ b/extensions/image-preview/src/preview.ts @@ -27,8 +27,8 @@ export class PreviewManager implements vscode.CustomEditorProvider { private readonly zoomStatusBarEntry: ZoomStatusBarEntry, ) { } - public async resolveCustomDocument(_document: vscode.CustomDocument): Promise { - // noop + public async openCustomDocument(uri: vscode.Uri) { + return new vscode.CustomDocument(PreviewManager.viewType, uri); } public async resolveCustomEditor( diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index d640b21131..af7051f357 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -63,6 +63,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview private _activePreview: DynamicMarkdownPreview | undefined = undefined; + private readonly customEditorViewType = 'vscode.markdown.preview.editor'; + public constructor( private readonly _contentProvider: MarkdownContentProvider, private readonly _logger: Logger, @@ -70,7 +72,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview ) { super(); 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() { @@ -148,8 +150,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview this.registerDynamicPreview(preview); } - public async resolveCustomDocument(_document: vscode.CustomDocument): Promise { - // noop + public async openCustomDocument(uri: vscode.Uri) { + return new vscode.CustomDocument(this.customEditorViewType, uri); } public async resolveCustomTextEditor( diff --git a/extensions/markdown-language-features/src/telemetryReporter.ts b/extensions/markdown-language-features/src/telemetryReporter.ts index 6d93a328e8..74b1e0e17b 100644 --- a/extensions/markdown-language-features/src/telemetryReporter.ts +++ b/extensions/markdown-language-features/src/telemetryReporter.ts @@ -48,12 +48,12 @@ export function loadDefaultTelemetryReporter(): TelemetryReporter { } function getPackageInfo(): IPackageInfo | null { - const extention = vscode.extensions.getExtension('Microsoft.vscode-markdown'); - if (extention && extention.packageJSON) { + const extension = vscode.extensions.getExtension('Microsoft.vscode-markdown'); + if (extension && extension.packageJSON) { return { - name: extention.packageJSON.name, - version: extention.packageJSON.version, - aiKey: extention.packageJSON.aiKey + name: extension.packageJSON.name, + version: extension.packageJSON.version, + aiKey: extension.packageJSON.aiKey }; } return null; diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/vscode-account/src/AADHelper.ts index d08cfcc3cf..34c4061840 100644 --- a/extensions/vscode-account/src/AADHelper.ts +++ b/extensions/vscode-account/src/AADHelper.ts @@ -54,7 +54,7 @@ function parseQuery(uri: vscode.Uri) { }, {}); } -export const onDidChangeSessions = new vscode.EventEmitter(); +export const onDidChangeSessions = new vscode.EventEmitter(); export const REFRESH_NETWORK_FAILURE = 'Network failure'; @@ -129,7 +129,8 @@ export class AzureActiveDirectoryService { private pollForChange() { setTimeout(async () => { - let didChange = false; + const addedIds: string[] = []; + let removedIds: string[] = []; const storedData = await keychain.getToken(); if (storedData) { try { @@ -139,7 +140,7 @@ export class AzureActiveDirectoryService { if (!matchesExisting) { try { await this.refreshToken(session.refreshToken, session.scope); - didChange = true; + addedIds.push(session.id); } catch (e) { if (e.message === REFRESH_NETWORK_FAILURE) { // 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); if (!matchesExisting) { await this.logout(token.sessionId); - didChange = true; + removedIds.push(token.sessionId); } })); @@ -162,19 +163,19 @@ export class AzureActiveDirectoryService { } catch (e) { Logger.error(e.message); // if data is improperly formatted, remove all of it and send change event + removedIds = this._tokens.map(token => token.sessionId); this.clearSessions(); - didChange = true; } } else { if (this._tokens.length) { // Log out all + removedIds = this._tokens.map(token => token.sessionId); await this.clearSessions(); - didChange = true; } } - if (didChange) { - onDidChangeSessions.fire(); + if (addedIds.length || removedIds.length) { + onDidChangeSessions.fire({ added: addedIds, removed: removedIds, changed: [] }); } this.pollForChange(); @@ -377,7 +378,7 @@ export class AzureActiveDirectoryService { this._refreshTimeouts.set(token.sessionId, setTimeout(async () => { try { await this.refreshToken(token.refreshToken, scope); - onDidChangeSessions.fire(); + onDidChangeSessions.fire({ added: [], removed: [], changed: [token.sessionId] }); } catch (e) { if (e.message === REFRESH_NETWORK_FAILURE) { const didSucceedOnRetry = await this.handleRefreshNetworkError(token.sessionId, token.refreshToken, scope); @@ -386,7 +387,7 @@ export class AzureActiveDirectoryService { } } else { await this.logout(token.sessionId); - onDidChangeSessions.fire(); + onDidChangeSessions.fire({ added: [], removed: [token.sessionId], changed: [] }); } } }, 1000 * (parseInt(token.expiresIn) - 30))); @@ -548,9 +549,8 @@ export class AzureActiveDirectoryService { const token = this._tokens.find(token => token.sessionId === sessionId); if (token) { token.accessToken = undefined; + onDidChangeSessions.fire({ added: [], removed: [], changed: [token.sessionId] }); } - - onDidChangeSessions.fire(); } const delayBeforeRetry = 5 * attempts * attempts; diff --git a/extensions/vscode-account/src/extension.ts b/extensions/vscode-account/src/extension.ts index 95b7ecc80e..4ed2955794 100644 --- a/extensions/vscode-account/src/extension.ts +++ b/extensions/vscode-account/src/extension.ts @@ -25,13 +25,17 @@ export async function activate(context: vscode.ExtensionContext) { login: async (scopes: string[]) => { try { 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]!; } catch (e) { throw e; } }, 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) { - await loginService.logout(loginService.sessions[0].id); - onDidChangeSessions.fire(); + const id = loginService.sessions[0].id; + await loginService.logout(id); + onDidChangeSessions.fire({ added: [], removed: [id], changed: [] }); vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out.")); return; } @@ -61,7 +66,7 @@ export async function activate(context: vscode.ExtensionContext) { if (selectedSession) { await loginService.logout(selectedSession.id); - onDidChangeSessions.fire(); + onDidChangeSessions.fire({ added: [], removed: [selectedSession.id], changed: [] }); vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out.")); return; } diff --git a/package.json b/package.json index 4196c23808..2399755db0 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ "vinyl": "^2.0.0", "vinyl-fs": "^3.0.0", "vsce": "1.48.0", - "vscode-debugprotocol": "1.39.0", + "vscode-debugprotocol": "1.40.0-pre.1", "vscode-nls-dev": "^3.3.1", "webpack": "^4.16.5", "webpack-cli": "^3.3.8", diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index 9912d07098..d20bf85bdf 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -65,7 +65,7 @@ export interface IIdentityProvider { export enum ListAriaRootRole { /** default list structure role */ - LIST = 'listbox', + LIST = 'list', /** default tree structure role */ TREE = 'tree', diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 532a5dd6cb..0a4dd4dc09 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -182,17 +182,19 @@ class Trait implements ISpliceable, IDisposable { class SelectionTrait extends Trait { - constructor() { + constructor(private setAriaSelected: boolean) { super('selected'); } renderIndex(index: number, container: HTMLElement): void { super.renderIndex(index, container); - if (this.contains(index)) { - container.setAttribute('aria-selected', 'true'); - } else { - container.setAttribute('aria-selected', 'false'); + if (this.setAriaSelected) { + if (this.contains(index)) { + container.setAttribute('aria-selected', 'true'); + } else { + container.setAttribute('aria-selected', 'false'); + } } } } @@ -1198,7 +1200,7 @@ export class List implements ISpliceable, IDisposable { renderers: IListRenderer[], private _options: IListOptions = DefaultOptions ) { - this.selection = new SelectionTrait(); + this.selection = new SelectionTrait(this._options.ariaRole !== 'listbox'); this.focus = new Trait('focused'); mixin(_options, defaultStyles, false); @@ -1501,9 +1503,13 @@ export class List implements ISpliceable, IDisposable { } 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; } - const index = this.findNextIndex(0, false, filter); + const index = this.findNextIndex(n, false, filter); if (index > -1) { this.setFocus([index], browserEvent); diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index 3a81ce296f..ee0537e1a6 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -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; }`); } - // Match quickOpen outline styles - ignore for disabled options + // Match quick input outline styles - ignore for disabled options 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; }`); } diff --git a/src/vs/base/browser/ui/splitview/paneview.ts b/src/vs/base/browser/ui/splitview/paneview.ts index 5df8c7b677..13ea6c26b8 100644 --- a/src/vs/base/browser/ui/splitview/paneview.ts +++ b/src/vs/base/browser/ui/splitview/paneview.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; 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 { Color, RGBA } from 'vs/base/common/color'; import { SplitView, IView } from './splitview'; @@ -52,7 +52,6 @@ export abstract class Pane extends Disposable implements IView { protected _expanded: boolean; protected _orientation: Orientation; - protected _preventCollapse?: boolean; private expandedSize: number | undefined = undefined; private _headerVisible = true; @@ -106,7 +105,7 @@ export abstract class Pane extends Disposable implements IView { get minimumSize(): number { const headerSize = this.headerSize; 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; } @@ -114,7 +113,7 @@ export abstract class Pane extends Disposable implements IView { get maximumSize(): number { const headerSize = this.headerSize; 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; } @@ -174,6 +173,18 @@ export abstract class Pane extends Disposable implements IView { this._onDidChange.fire(undefined); } + get orientation(): Orientation { + return this._orientation; + } + + set orientation(orientation: Orientation) { + if (this._orientation === orientation) { + return; + } + + this._orientation = orientation; + } + render(): void { this.header = $('.pane-header'); append(this.element, this.header); @@ -190,22 +201,20 @@ export abstract class Pane extends Disposable implements IView { this.updateHeader(); - if (!this._preventCollapse) { - const onHeaderKeyDown = Event.chain(domEvent(this.header, 'keydown')) - .map(e => new StandardKeyboardEvent(e)); + const onHeaderKeyDown = Event.chain(domEvent(this.header, 'keydown')) + .map(e => new StandardKeyboardEvent(e)); - this._register(onHeaderKeyDown.filter(e => e.keyCode === KeyCode.Enter || e.keyCode === KeyCode.Space) - .event(() => this.setExpanded(!this.isExpanded()), null)); + this._register(onHeaderKeyDown.filter(e => e.keyCode === KeyCode.Enter || e.keyCode === KeyCode.Space) + .event(() => this.setExpanded(!this.isExpanded()), null)); - this._register(onHeaderKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow) - .event(() => this.setExpanded(false), null)); + this._register(onHeaderKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow) + .event(() => this.setExpanded(false), null)); - this._register(onHeaderKeyDown.filter(e => e.keyCode === KeyCode.RightArrow) - .event(() => this.setExpanded(true), null)); + this._register(onHeaderKeyDown.filter(e => e.keyCode === KeyCode.RightArrow) + .event(() => this.setExpanded(true), null)); - this._register(domEvent(this.header, 'click') - (() => this.setExpanded(!this.isExpanded()), null)); - } + this._register(domEvent(this.header, 'click') + (() => this.setExpanded(!this.isExpanded()), null)); this.body = append(this.element, $('.pane-body')); this.renderBody(this.body); @@ -402,13 +411,14 @@ export class PaneView extends Disposable { private el: HTMLElement; private paneItems: IPaneItem[] = []; private orthogonalSize: number = 0; + private size: number = 0; private splitview: SplitView; - private orientation: Orientation; private animationTimer: number | undefined = undefined; private _onDidDrop = this._register(new Emitter<{ from: Pane, to: Pane }>()); readonly onDidDrop: Event<{ from: Pane, to: Pane }> = this._onDidDrop.event; + orientation: Orientation; readonly onDidSashChange: Event; constructor(container: HTMLElement, options: IPaneViewOptions = {}) { @@ -427,6 +437,7 @@ export class PaneView extends Disposable { const paneItem = { pane: pane, disposable: disposables }; this.paneItems.splice(index, 0, paneItem); + pane.orientation = this.orientation; pane.orthogonalSize = this.orthogonalSize; this.splitview.addView(pane, size, index); @@ -485,12 +496,39 @@ export class PaneView extends Disposable { layout(height: number, width: number): void { this.orthogonalSize = this.orientation === Orientation.VERTICAL ? width : height; + this.size = this.orientation === Orientation.HORIZONTAL ? width : height; for (const paneItem of this.paneItems) { 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 { diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index 1bc9a4d41e..e412009eeb 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -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 { return ( source[offset] * 2 ** 24 @@ -134,11 +147,11 @@ export function writeUInt32BE(destination: Uint8Array, value: number, offset: nu destination[offset] = value; } -function readUInt8(source: Uint8Array, offset: number): number { +export function readUInt8(source: Uint8Array, offset: number): number { 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; } diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index da265d5e24..6c72d39c92 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -321,6 +321,8 @@ export function prepareQuery(original: string): IPreparedQuery { let value = stripWildcards(original).replace(/\s/g, ''); // get rid of all wildcards and whitespace if (isWindows) { 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(); @@ -451,7 +453,7 @@ function doScoreItem(label: string, description: string | undefined, path: strin return NO_ITEM_SCORE; } -export function compareItemsByScore(itemA: T, itemB: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor, cache: ScorerCache, fallbackComparer = fallbackCompare): number { +export function compareItemsByScore(itemA: T, itemB: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor, cache: ScorerCache): number { const itemScoreA = scoreItem(itemA, query, fuzzy, accessor, cache); const itemScoreB = scoreItem(itemB, query, fuzzy, accessor, cache); @@ -517,7 +519,16 @@ export function compareItemsByScore(itemA: T, itemB: T, query: IPreparedQuery 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 itemBMatchDistance = computeLabelAndDescriptionMatchDistance(itemB, itemScoreB, accessor); if (itemAMatchDistance && itemBMatchDistance && itemAMatchDistance !== itemBMatchDistance) { @@ -526,7 +537,7 @@ export function compareItemsByScore(itemA: T, itemB: T, query: IPreparedQuery // 7.) at this point, scores are identical and match compactness as well // 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(item: T, score: IItemScore, accessor: IItemAccessor): number { diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index af3a8d9333..93eeff2ef3 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -53,6 +53,12 @@ export namespace Schemas { export const vscodeRemoteResource = 'vscode-remote-resource'; export const userData = 'vscode-userdata'; + + export const vscodeCustomEditor = 'vscode-custom-editor'; + + export const vscodeSettings = 'vscode-settings'; + + export const webviewPanel = 'webview-panel'; } class RemoteAuthoritiesImpl { diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index d94f1b6d4c..a89727f39c 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -209,3 +209,17 @@ export const enum OperatingSystem { Linux = 3 } 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; +} diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index a756d4ef76..c14dec88b9 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -15,8 +15,16 @@ import { TernarySearchTree } from 'vs/base/common/map'; 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 { @@ -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. + * URI queries must match, fragments are ignored. * @param base A uri which is "longer" * @param parentCandidate A uri which is "shorter" then `base` */ export function isEqualOrParent(base: URI, parentCandidate: URI, ignoreCase = hasToIgnoreCase(base)): boolean { if (base.scheme === parentCandidate.scheme) { 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)) { - return extpath.isEqualOrParent(base.path, parentCandidate.path, ignoreCase, '/'); + return extpath.isEqualOrParent(base.path || '/', parentCandidate.path || '/', ignoreCase, '/') && base.query === parentCandidate.query; } } 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) { 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) { return true; } @@ -67,7 +79,7 @@ export function isEqual(first: URI | undefined, second: URI | undefined, ignoreC } 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 { @@ -88,13 +100,15 @@ export function dirname(resource: URI): URI { if (resource.path.length === 0) { return resource; } + let dirname; if (resource.scheme === Schemas.file) { - return URI.file(paths.dirname(originalFSPath(resource))); - } - let dirname = paths.posix.dirname(resource.path); - if (resource.authority && dirname.length && dirname.charCodeAt(0) !== CharCode.Slash) { - 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 = URI.file(paths.dirname(originalFSPath(resource))).path; + } else { + dirname = paths.posix.dirname(resource.path); + if (resource.authority && dirname.length && dirname.charCodeAt(0) !== CharCode.Slash) { + 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 + } } return resource.with({ 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. * 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)) { return undefined; } @@ -198,7 +212,7 @@ export function relativePath(from: URI, to: URI, ignoreCase = hasToIgnoreCase(fr return isWindows ? extpath.toSlashes(relativePath) : relativePath; } let fromPath = from.path || '/', toPath = to.path || '/'; - if (ignoreCase) { + if (caseInsensitivePath) { // make casing of fromPath match toPath let i = 0; for (const len = Math.min(fromPath.length, toPath.length); i < len; i++) { diff --git a/src/vs/base/parts/quickinput/browser/media/quickInput.css b/src/vs/base/parts/quickinput/browser/media/quickInput.css index 2971e01c42..fe8b61e2a9 100644 --- a/src/vs/base/parts/quickinput/browser/media/quickInput.css +++ b/src/vs/base/parts/quickinput/browser/media/quickInput.css @@ -55,8 +55,8 @@ margin-bottom: -2px; } -.quick-input-widget.quick-navigate-mode .quick-input-header { - /* reduce margins and paddings in quick navigate mode */ +.quick-input-widget.hidden-input .quick-input-header { + /* reduce margins and paddings when input box hidden */ padding: 0; margin-bottom: 0; } @@ -132,8 +132,8 @@ margin-top: 6px; } -.quick-input-widget.quick-navigate-mode .quick-input-list { - margin-top: 0; /* reduce margins in quick navigate mode */ +.quick-input-widget.hidden-input .quick-input-list { + margin-top: 0; /* reduce margins when input box hidden */ } .quick-input-list .monaco-list { diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index ce47bd546e..bf4e4d21d3 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -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 * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { QuickInputList } from './quickInputList'; +import { QuickInputList, QuickInputListFocus } from './quickInputList'; import { QuickInputBox } from './quickInputBox'; import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -364,7 +364,7 @@ class QuickInput extends Disposable implements IQuickInput { readonly onDispose = this.onDisposeEmitter.event; - public dispose(): void { + dispose(): void { this.hide(); this.onDisposeEmitter.fire(); @@ -391,6 +391,7 @@ class QuickPick extends QuickInput implements IQuickPi private _matchOnLabel = true; private _sortByLabel = true; private _autoFocusOnList = true; + private _autoFocusSecondEntry = false; private _activeItems: T[] = []; private activeItemsUpdated = false; private activeItemsToConfirm: T[] | null = []; @@ -408,6 +409,7 @@ class QuickPick extends QuickInput implements IQuickPi private _customButtonLabel: string | undefined; private _customButtonHover: string | undefined; private _quickNavigate: IQuickNavigateConfiguration | undefined; + private _hideInput: boolean | undefined; get quickNavigate() { return this._quickNavigate; @@ -460,10 +462,6 @@ class QuickPick extends QuickInput implements IQuickPi set items(items: Array) { this._items = items; this.itemsUpdated = true; - if (this._items.length === 0) { - // quick-navigate requires at least 1 item - this._quickNavigate = undefined; - } this.update(); } @@ -520,7 +518,6 @@ class QuickPick extends QuickInput implements IQuickPi this.update(); } - get autoFocusOnList() { return this._autoFocusOnList; } @@ -530,6 +527,14 @@ class QuickPick extends QuickInput implements IQuickPi this.update(); } + get autoFocusSecondEntry() { + return this._autoFocusSecondEntry; + } + + set autoFocusSecondEntry(autoFocusSecondEntry: boolean) { + this._autoFocusSecondEntry = autoFocusSecondEntry; + } + get activeItems() { return this._activeItems; } @@ -614,14 +619,23 @@ class QuickPick extends QuickInput implements IQuickPi this.update(); } - public inputHasFocus(): boolean { + inputHasFocus(): boolean { return this.visible ? this.ui.inputBox.hasFocus() : false; } - public focusOnInput() { + focusOnInput() { this.ui.inputBox.setFocus(); } + get hideInput() { + return !!this._hideInput; + } + + set hideInput(hideInput: boolean) { + this._hideInput = hideInput; + this.update(); + } + onDidChangeSelection = this.onDidChangeSelectionEmitter.event; onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event; @@ -629,7 +643,7 @@ class QuickPick extends QuickInput implements IQuickPi private trySelectFirst() { if (this.autoFocusOnList) { if (!this.ui.isScreenReaderOptimized() && !this.canSelectMany) { - this.ui.list.focus('First'); + this.ui.list.focus(QuickInputListFocus.First); } } } @@ -656,7 +670,7 @@ class QuickPick extends QuickInput implements IQuickPi this.visibleDisposables.add(this.ui.inputBox.onKeyDown(event => { switch (event.keyCode) { case KeyCode.DownArrow: - this.ui.list.focus('Next'); + this.ui.list.focus(QuickInputListFocus.Next); if (this.canSelectMany) { this.ui.list.domFocus(); } @@ -664,9 +678,9 @@ class QuickPick extends QuickInput implements IQuickPi break; case KeyCode.UpArrow: if (this.ui.list.getFocusedElements().length) { - this.ui.list.focus('Previous'); + this.ui.list.focus(QuickInputListFocus.Previous); } else { - this.ui.list.focus('Last'); + this.ui.list.focus(QuickInputListFocus.Last); } if (this.canSelectMany) { this.ui.list.domFocus(); @@ -675,9 +689,9 @@ class QuickPick extends QuickInput implements IQuickPi break; case KeyCode.PageDown: if (this.ui.list.getFocusedElements().length) { - this.ui.list.focus('NextPage'); + this.ui.list.focus(QuickInputListFocus.NextPage); } else { - this.ui.list.focus('First'); + this.ui.list.focus(QuickInputListFocus.First); } if (this.canSelectMany) { this.ui.list.domFocus(); @@ -686,9 +700,9 @@ class QuickPick extends QuickInput implements IQuickPi break; case KeyCode.PageUp: if (this.ui.list.getFocusedElements().length) { - this.ui.list.focus('PreviousPage'); + this.ui.list.focus(QuickInputListFocus.PreviousPage); } else { - this.ui.list.focus('Last'); + this.ui.list.focus(QuickInputListFocus.Last); } if (this.canSelectMany) { this.ui.list.domFocus(); @@ -721,7 +735,7 @@ class QuickPick extends QuickInput implements IQuickPi this.onDidAcceptEmitter.fire({ inBackground: false }); })); this.visibleDisposables.add(this.ui.onDidCustom(() => { - this.onDidCustomEmitter.fire(undefined); + this.onDidCustomEmitter.fire(); })); this.visibleDisposables.add(this.ui.list.onDidChangeFocus(focusedItems => { if (this.activeItemsUpdated) { @@ -768,7 +782,7 @@ class QuickPick extends QuickInput implements IQuickPi private registerQuickNavigation() { return dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, e => { - if (this.canSelectMany || !this.quickNavigate) { + if (this.canSelectMany || !this._quickNavigate) { return; } @@ -776,7 +790,7 @@ class QuickPick extends QuickInput implements IQuickPi const keyCode = keyboardEvent.keyCode; // 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 [firstPart, chordPart] = k.getParts(); if (chordPart) { @@ -806,10 +820,16 @@ class QuickPick extends QuickInput implements IQuickPi return false; }); - if (wasTriggerKeyPressed && this.activeItems[0]) { - this._selectedItems = [this.activeItems[0]]; - this.onDidChangeSelectionEmitter.fire(this.selectedItems); - this.onDidAcceptEmitter.fire({ inBackground: false }); + if (wasTriggerKeyPressed) { + if (this.activeItems[0]) { + this._selectedItems = [this.activeItems[0]]; + 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 extends QuickInput implements IQuickPi if (!this.visible) { return; } - dom.toggleClass(this.ui.container, 'quick-navigate-mode', !!this._quickNavigate); - const ok = this.ok === 'default' ? this.canSelectMany : this.ok; - const visibilities: Visibilities = this.canSelectMany ? - { 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, description: !!this.description, inputBox: !this._quickNavigate, progressBar: !this._quickNavigate, visibleCount: true, list: true, message: !!this.validationMessage, customButton: this.customButton, ok }; + const hideInput = !!this._hideInput && this._items.length > 0; // do not allow to hide input without items + dom.toggleClass(this.ui.container, 'hidden-input', hideInput); + const visibilities: Visibilities = { + title: !!this.title || !!this.step, + 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); super.update(); if (this.ui.inputBox.value !== this.value) { @@ -844,17 +874,16 @@ class QuickPick extends QuickInput implements IQuickPi this.ui.list.sortByLabel = this.sortByLabel; if (this.itemsUpdated) { this.itemsUpdated = false; - const previousItemCount = this.ui.list.getElementsCount(); this.ui.list.setElements(this.items); this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); this.ui.visibleCount.setCount(this.ui.list.getVisibleCount()); this.ui.count.setCount(this.ui.list.getCheckedCount()); - this.trySelectFirst(); - if (this._quickNavigate && previousItemCount === 0 && this.items.length > 1) { - // quick navigate: automatically focus the second entry - // so that upon release the item is picked directly - this.ui.list.focus('Next'); + if (this._autoFocusSecondEntry) { + this.ui.list.focus(QuickInputListFocus.Second); + this._autoFocusSecondEntry = false; // only valid once, then unset + } else { + this.trySelectFirst(); } } if (this.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) { @@ -985,7 +1014,7 @@ class InputBox extends QuickInput implements IInputBox { this._value = 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; } super.show(); @@ -1039,10 +1068,12 @@ export class QuickInputController extends Disposable { private parentElement: HTMLElement; private styles: IQuickInputStyles; + private onShowEmitter = new Emitter(); + readonly onShow = this.onShowEmitter.event; + private onHideEmitter = new Emitter(); - public onShow = this.onShowEmitter.event; - public onHide = this.onHideEmitter.event; + readonly onHide = this.onHideEmitter.event; constructor(private options: IQuickInputOptions) { super(); @@ -1517,7 +1548,7 @@ export class QuickInputController extends Disposable { } } - public hide(focusLost?: boolean) { + hide(focusLost?: boolean) { const controller = this.controller; if (controller) { this.controller = null; @@ -1544,14 +1575,21 @@ export class QuickInputController extends Disposable { navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration) { 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) { 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(); } @@ -1583,7 +1621,7 @@ export class QuickInputController extends Disposable { } } - public applyStyles(styles: IQuickInputStyles) { + applyStyles(styles: IQuickInputStyles) { this.styles = styles; this.updateStyles(); } diff --git a/src/vs/base/parts/quickinput/browser/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts index fbb48eb4e5..fb0e4f1fdd 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -222,6 +222,16 @@ class ListElementDelegate implements IListVirtualDelegate { } } +export enum QuickInputListFocus { + First = 1, + Second, + Last, + Next, + Previous, + NextPage, + PreviousPage +} + export class QuickInputList { readonly id: string; @@ -307,6 +317,18 @@ export class QuickInputList { 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 @@ -430,7 +452,10 @@ export class QuickInputList { .filter(item => this.elementsToIndexes.has(item)) .map(item => this.elementsToIndexes.get(item)!)); 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'; } - focus(what: 'First' | 'Last' | 'Next' | 'Previous' | 'NextPage' | 'PreviousPage'): void { + focus(what: QuickInputListFocus): void { if (!this.list.length) { return; } - if ((what === 'Next' || what === 'NextPage') && this.list.getFocus()[0] === this.list.length - 1) { - what = 'First'; + if ((what === QuickInputListFocus.Next || what === QuickInputListFocus.NextPage) && this.list.getFocus()[0] === this.list.length - 1) { + 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() { diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index e37f20edda..9e9934922e 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -237,6 +237,14 @@ export interface IQuickPick extends IQuickInput { 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; activeItems: ReadonlyArray; @@ -256,6 +264,13 @@ export interface IQuickPick extends IQuickInput { inputHasFocus(): boolean; 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 { diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 56e3ac6309..f6a62ce54c 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -8,6 +8,7 @@ import * as scorer from 'vs/base/common/fuzzyScorer'; import { URI } from 'vs/base/common/uri'; import { basename, dirname, sep } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; +import { Schemas } from 'vs/base/common/network'; class ResourceAccessorClass implements scorer.IItemAccessor { @@ -49,8 +50,8 @@ function scoreItem(item: T, query: string, fuzzy: boolean, accessor: scorer.I return scorer.scoreItem(item, scorer.prepareQuery(query), fuzzy, accessor, cache); } -function compareItemsByScore(itemA: T, itemB: T, query: string, fuzzy: boolean, accessor: scorer.IItemAccessor, cache: scorer.ScorerCache, fallbackComparer?: (itemA: T, itemB: T, query: scorer.IPreparedQuery, accessor: scorer.IItemAccessor) => number): number { - return scorer.compareItemsByScore(itemA, itemB, scorer.prepareQuery(query), fuzzy, accessor, cache, fallbackComparer as any); +function compareItemsByScore(itemA: T, itemB: T, query: string, fuzzy: boolean, accessor: scorer.IItemAccessor, cache: scorer.ScorerCache): number { + return scorer.compareItemsByScore(itemA, itemB, scorer.prepareQuery(query), fuzzy, accessor, cache); } const NullAccessor = new NullAccessorClass(); @@ -279,6 +280,19 @@ suite('Fuzzy Scorer', () => { 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 () { const resourceA = URI.file('/some/path/fileA.txt'); const resourceB = URI.file('/some/path/other/fileB.txt'); @@ -509,33 +523,13 @@ suite('Fuzzy Scorer', () => { assert.equal(res[2], resourceC); }); - test('compareFilesByScore - allow to provide fallback sorter (bug #31591)', function () { - const resourceA = URI.file('virtual/vscode.d.ts'); - const resourceB = URI.file('vscode/src/vs/vscode.d.ts'); + test('compareFilesByScore - prefer matches in label over description if scores are otherwise equal', function () { + const resourceA = URI.file('parts/quick/arrow-left-dark.svg'); + const resourceB = URI.file('parts/quickopen/quickopen.ts'); - let query = 'vscode'; + let query = 'partsquick'; - let res = [resourceA, resourceB].sort((r1, r2) => { - 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; - }); - }); + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceB); assert.equal(res[1], resourceA); }); diff --git a/src/vs/base/test/common/resources.test.ts b/src/vs/base/test/common/resources.test.ts index 39d91ed1f5..7776bee61e 100644 --- a/src/vs/base/test/common/resources.test.ts +++ b/src/vs/base/test/common/resources.test.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 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 { isWindows } from 'vs/base/common/platform'; import { toSlashes } from 'vs/base/common/extpath'; @@ -66,6 +66,8 @@ suite('Resources', () => { // does not explode (https://github.com/Microsoft/vscode/issues/41987) 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', () => { @@ -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')).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', () => { @@ -233,7 +236,7 @@ suite('Resources', () => { }); 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); } } @@ -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/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://a2/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', () => { let fileURI = 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); - assert.equal(isEqual(fileURI, fileURI, false), true); - assert.equal(isEqual(fileURI, fileURI, hasToIgnoreCase(fileURI)), true); - assert.equal(isEqual(fileURI, fileURI2, true), true); - assert.equal(isEqual(fileURI, fileURI2, false), false); + assertIsEqual(fileURI, fileURI, true, true); + assertIsEqual(fileURI, fileURI, false, true); + assertIsEqual(fileURI, fileURI, hasToIgnoreCase(fileURI), true); + assertIsEqual(fileURI, fileURI2, true, true); + assertIsEqual(fileURI, fileURI2, false, false); let fileURI3 = URI.parse('foo://server:453/foo/bar'); let fileURI4 = URI.parse('foo://server:453/foo/Bar'); - assert.equal(isEqual(fileURI3, fileURI3, true), true); - assert.equal(isEqual(fileURI3, fileURI3, false), true); - assert.equal(isEqual(fileURI3, fileURI3, hasToIgnoreCase(fileURI3)), true); - assert.equal(isEqual(fileURI3, fileURI4, true), true); - assert.equal(isEqual(fileURI3, fileURI4, false), false); + assertIsEqual(fileURI3, fileURI3, true, true); + assertIsEqual(fileURI3, fileURI3, false, true); + assertIsEqual(fileURI3, fileURI3, hasToIgnoreCase(fileURI3), true); + assertIsEqual(fileURI3, fileURI4, true, true); + 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', () => { @@ -388,5 +409,12 @@ suite('Resources', () => { assert.equal(isEqualOrParent(fileURI3, fileURI4, false), true, '14'); assert.equal(isEqualOrParent(fileURI3, fileURI, true), false, '15'); 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'); }); }); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 694d68ff19..4f17847f81 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -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 { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; 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 { LoggerService } from 'vs/platform/log/node/loggerService'; 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); 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 userDataSyncChannel = new UserDataSyncChannel(userDataSyncService); server.registerChannel('userDataSync', userDataSyncChannel); diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index 814479ff15..1195461f30 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -30,6 +30,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { scrollbarShadow } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { Constants } from 'vs/base/common/uint'; const DIFF_LINES_PADDING = 3; @@ -124,16 +125,6 @@ export class DiffReview extends Disposable { } 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) => { e.preventDefault(); @@ -209,7 +200,9 @@ export class DiffReview extends Disposable { } 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._diffEditor.doLayout(); this._render(); @@ -242,7 +235,9 @@ export class DiffReview extends Disposable { } 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._diffEditor.doLayout(); this._render(); @@ -551,6 +546,7 @@ export class DiffReview extends Disposable { let container = document.createElement('div'); container.className = 'diff-review-table'; container.setAttribute('role', 'list'); + container.setAttribute('aria-label', 'Difference review. Use "Stage | Unstage | Revert Selected Ranges" commands'); Configuration.applyFontInfoSlow(container, modifiedOptions.get(EditorOption.fontInfo)); let minOriginalLine = 0; @@ -590,11 +586,11 @@ export class DiffReview extends Disposable { const getAriaLines = (lines: number) => { if (lines === 0) { - return nls.localize('no_lines', "no lines"); + return nls.localize('no_lines_changed', "no lines changed"); } else if (lines === 1) { - return nls.localize('one_line', "1 line"); + return nls.localize('one_line_changed', "1 line changed"); } 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.', '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 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); // @@ -504,7 +517,7 @@ diff --git a/src/vs/editor/common/core/stringBuilder.ts b/src/vs/editor/common/core/stringBuilder.ts index 3b53d1aa0e..ce2f656a65 100644 --- a/src/vs/editor/common/core/stringBuilder.ts +++ b/src/vs/editor/common/core/stringBuilder.ts @@ -4,8 +4,13 @@ *--------------------------------------------------------------------------------------------*/ 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 { decode(view: Uint16Array): string; } @@ -18,17 +23,42 @@ export interface IStringBuilder { 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 decodeUTF16LE: (source: Uint8Array, offset: number, len: number) => string; if (typeof TextDecoder !== 'undefined') { createStringBuilder = (capacity) => new StringBuilder(capacity); + decodeUTF16LE = standardDecodeUTF16LE; } else { 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 { - private readonly _decoder: TextDecoder; private readonly _capacity: number; private readonly _buffer: Uint16Array; @@ -36,7 +66,6 @@ class StringBuilder implements IStringBuilder { private _bufferLength: number; constructor(capacity: number) { - this._decoder = new TextDecoder('UTF-16LE'); this._capacity = capacity | 0; this._buffer = new Uint16Array(this._capacity); @@ -63,7 +92,7 @@ class StringBuilder implements IStringBuilder { } const view = new Uint16Array(this._buffer.buffer, 0, this._bufferLength); - return this._decoder.decode(view); + return getPlatformTextDecoder().decode(view); } private _flushBuffer(): void { diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 88c2c23b0e..cb8f0f5cb5 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -15,6 +15,7 @@ import { SearchData } from 'vs/editor/common/model/textModelSearch'; import { LanguageId, LanguageIdentifier, FormattingOptions } from 'vs/editor/common/modes'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; 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. @@ -373,21 +374,13 @@ export interface IValidEditOperation { */ 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. + * @internal */ - forceMoveMarkers: boolean; -} - -/** - * @internal - */ -export interface IValidEditOperations { - operations: IValidEditOperation[]; + textChange: TextChange; } /** @@ -1099,9 +1092,11 @@ export interface ITextModel { * 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. * @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. @@ -1112,7 +1107,12 @@ export interface ITextModel { /** * @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`. @@ -1291,7 +1291,7 @@ export interface ITextBuffer { getLineLastNonWhitespaceColumn(lineNumber: number): number; 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[]; } @@ -1301,7 +1301,7 @@ export interface ITextBuffer { export class ApplyEditsResult { constructor( - public readonly reverseEdits: IValidEditOperation[], + public readonly reverseEdits: IValidEditOperation[] | null, public readonly changes: IInternalModelContentChange[], public readonly trimAutoWhitespaceLineNumbers: number[] | null ) { } diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index fa076bb026..7cfb281313 100644 --- a/src/vs/editor/common/model/editStack.ts +++ b/src/vs/editor/common/model/editStack.ts @@ -6,69 +6,209 @@ import * as nls from 'vs/nls'; import { onUnexpectedError } from 'vs/base/common/errors'; 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 { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; 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 readonly label: string; - private _isOpen: boolean; - public readonly model: ITextModel; - private readonly _beforeVersionId: number; - private readonly _beforeEOL: EndOfLineSequence; - private readonly _beforeCursorState: Selection[] | null; - private _afterVersionId: number; - private _afterEOL: EndOfLineSequence; - private _afterCursorState: Selection[] | null; - private _edits: IValidEditOperations[]; - - public get resource(): URI { - return this.model.uri; + public static create(model: ITextModel, beforeCursorState: Selection[] | null): SingleModelEditStackData { + const alternativeVersionId = model.getAlternativeVersionId(); + const eol = getModelEOL(model); + return new SingleModelEditStackData( + alternativeVersionId, + alternativeVersionId, + eol, + eol, + beforeCursorState, + beforeCursorState, + [] + ); } - constructor(model: ITextModel, beforeCursorState: Selection[] | null) { - this.label = nls.localize('edit', "Typing"); - this._isOpen = true; - this.model = model; - this._beforeVersionId = this.model.getAlternativeVersionId(); - this._beforeEOL = getModelEOL(this.model); - this._beforeCursorState = beforeCursorState; - this._afterVersionId = this._beforeVersionId; - this._afterEOL = this._beforeEOL; - this._afterCursorState = this._beforeCursorState; - this._edits = []; - } - - public canAppend(model: ITextModel): boolean { - return (this._isOpen && this.model === model); - } + constructor( + public readonly beforeVersionId: number, + public afterVersionId: number, + public readonly beforeEOL: EndOfLineSequence, + public afterEOL: EndOfLineSequence, + public readonly beforeCursorState: Selection[] | null, + public afterCursorState: Selection[] | null, + public changes: TextChange[] + ) { } public append(model: ITextModel, operations: IValidEditOperation[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void { 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 { - this._isOpen = false; + if (this._data instanceof SingleModelEditStackData) { + this._data = this._data.serialize(); + } } public undo(): void { - this._isOpen = false; - this._edits.reverse(); - this._edits = this.model._applyUndoRedoEdits(this._edits, this._beforeEOL, true, false, this._beforeVersionId, this._beforeCursorState); + if (URI.isUri(this.model)) { + // 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._applyUndo(data.changes, data.beforeEOL, data.beforeVersionId, data.beforeCursorState); } public redo(): void { - this._edits.reverse(); - this._edits = this.model._applyUndoRedoEdits(this._edits, this._afterEOL, false, true, this._afterVersionId, this._afterCursorState); + if (URI.isUri(this.model)) { + // 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; private _isOpen: boolean; - private readonly _editStackElementsArr: EditStackElement[]; - private readonly _editStackElementsMap: Map; + private readonly _editStackElementsArr: SingleModelEditStackElement[]; + private readonly _editStackElementsMap: Map; public get resources(): readonly URI[] { - return this._editStackElementsArr.map(editStackElement => editStackElement.model.uri); + return this._editStackElementsArr.map(editStackElement => editStackElement.resource); } constructor( label: string, - editStackElements: EditStackElement[] + editStackElements: SingleModelEditStackElement[] ) { this.label = label; this._isOpen = true; this._editStackElementsArr = editStackElements.slice(0); - this._editStackElementsMap = new Map(); + this._editStackElementsMap = new Map(); for (const editStackElement of this._editStackElementsArr) { - const key = uriGetComparisonKey(editStackElement.model.uri); + const key = uriGetComparisonKey(editStackElement.resource); 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 { if (!this._isOpen) { 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[] { return this._editStackElementsArr; } } +export type EditStackElement = SingleModelEditStackElement | MultiModelEditStackElement; + function getModelEOL(model: ITextModel): EndOfLineSequence { const eol = model.getEOL(); 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) { return false; } - return ((element instanceof EditStackElement) || (element instanceof MultiModelEditStackElement)); + return ((element instanceof SingleModelEditStackElement) || (element instanceof MultiModelEditStackElement)); } export class EditStack { @@ -177,12 +335,12 @@ export class EditStack { 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); if (isKnownStackElement(lastElement) && lastElement.canAppend(this._model)) { return lastElement; } - const newElement = new EditStackElement(this._model, beforeCursorState); + const newElement = new SingleModelEditStackElement(this._model, beforeCursorState); this._undoRedoService.pushElement(newElement); return newElement; } @@ -195,7 +353,7 @@ export class EditStack { public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer | null): Selection[] | null { const editStackElement = this._getOrCreateEditStackElement(beforeCursorState); - const inverseEditOperations = this._model.applyEdits(editOperations); + const inverseEditOperations = this._model.applyEdits(editOperations, true); const afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperations); editStackElement.append(this._model, inverseEditOperations, getModelEOL(this._model), this._model.getAlternativeVersionId(), afterCursorState); return afterCursorState; diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index 9815baa5a3..07038c43a2 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -269,7 +269,7 @@ export class PieceTreeBase { protected _buffers!: StringBuffer[]; // 0 is change buffer, others are readonly original buffer. protected _lineCnt!: number; protected _length!: number; - protected _EOL!: string; + protected _EOL!: '\r\n' | '\n'; protected _EOLLength!: number; protected _EOLNormalized!: boolean; private _lastChangeBufferPos!: BufferCursor; @@ -351,7 +351,7 @@ export class PieceTreeBase { } // #region Buffer API - public getEOL(): string { + public getEOL(): '\r\n' | '\n' { return this._EOL; } diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts index 8a184ddd4f..79a15b17be 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -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 { PieceTreeBase, StringBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; 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 { sortIndex: number; @@ -16,7 +18,10 @@ export interface IValidatedEditOperation { range: Range; rangeOffset: number; rangeLength: number; - lines: string[] | null; + text: string; + eolCount: number; + firstLineLength: number; + lastLineLength: number; forceMoveMarkers: boolean; isAutoWhitespaceEdit: boolean; } @@ -60,7 +65,7 @@ export class PieceTreeTextBuffer implements ITextBuffer { public getBOM(): string { return this._BOM; } - public getEOL(): string { + public getEOL(): '\r\n' | '\n' { return this._pieceTree.getEOL(); } @@ -201,7 +206,7 @@ export class PieceTreeTextBuffer implements ITextBuffer { 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 mightContainNonBasicASCII = this._mightContainNonBasicASCII; let canReduceOperations = true; @@ -220,13 +225,34 @@ export class PieceTreeTextBuffer implements ITextBuffer { if (!mightContainNonBasicASCII && 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] = { sortIndex: i, identifier: op.identifier || null, range: validatedRange, rangeOffset: this.getOffsetAt(validatedRange.startLineNumber, validatedRange.startColumn), 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), isAutoWhitespaceEdit: op.isAutoWhitespaceEdit || false }; @@ -254,46 +280,56 @@ export class PieceTreeTextBuffer implements ITextBuffer { } // Delta encode operations - let reverseRanges = PieceTreeTextBuffer._getInverseEditRanges(operations); + let reverseRanges = (computeUndoEdits || recordTrimAutoWhitespace ? PieceTreeTextBuffer._getInverseEditRanges(operations) : []); 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++) { - let op = operations[i]; - let reverseRange = reverseRanges[i]; - - if (recordTrimAutoWhitespace && op.isAutoWhitespaceEdit && op.range.isEmpty()) { - // Record already the future line numbers that might be auto whitespace removal candidates on next edit - for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) { - let currentLineContent = ''; - if (lineNumber === reverseRange.startLineNumber) { - currentLineContent = this.getLineContent(op.range.startLineNumber); - if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) { - continue; + if (op.isAutoWhitespaceEdit && op.range.isEmpty()) { + // Record already the future line numbers that might be auto whitespace removal candidates on next edit + for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) { + let currentLineContent = ''; + 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[] = []; - for (let i = 0; i < operations.length; i++) { - let op = operations[i]; - let reverseRange = reverseRanges[i]; + let reverseOperations: IReverseSingleEditOperation[] | null = null; + if (computeUndoEdits) { - reverseOperations[i] = { - sortIndex: op.sortIndex, - identifier: op.identifier, - range: reverseRange, - text: this.getValueInRange(op.range), - forceMoveMarkers: op.forceMoveMarkers - }; + let reverseRangeDeltaOffset = 0; + reverseOperations = []; + for (let i = 0; i < operations.length; i++) { + const op = operations[i]; + const reverseRange = reverseRanges[i]; + 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._mightContainNonBasicASCII = mightContainNonBasicASCII; @@ -350,58 +386,45 @@ export class PieceTreeTextBuffer implements ITextBuffer { } _toSingleEditOperation(operations: IValidatedEditOperation[]): IValidatedEditOperation { - let forceMoveMarkers = false, - firstEditRange = operations[0].range, - lastEditRange = operations[operations.length - 1].range, - entireEditRange = new Range(firstEditRange.startLineNumber, firstEditRange.startColumn, lastEditRange.endLineNumber, lastEditRange.endColumn), - lastEndLineNumber = firstEditRange.startLineNumber, - lastEndColumn = firstEditRange.startColumn, - result: string[] = []; + let forceMoveMarkers = false; + const firstEditRange = operations[0].range; + const lastEditRange = operations[operations.length - 1].range; + const entireEditRange = new Range(firstEditRange.startLineNumber, firstEditRange.startColumn, lastEditRange.endLineNumber, lastEditRange.endColumn); + let lastEndLineNumber = firstEditRange.startLineNumber; + let lastEndColumn = firstEditRange.startColumn; + const result: string[] = []; for (let i = 0, len = operations.length; i < len; i++) { - let operation = operations[i], - range = operation.range; + const operation = operations[i]; + const range = operation.range; forceMoveMarkers = forceMoveMarkers || operation.forceMoveMarkers; // (1) -- Push old text - for (let lineNumber = lastEndLineNumber; lineNumber < range.startLineNumber; lineNumber++) { - 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)); - } + result.push(this.getValueInRange(new Range(lastEndLineNumber, lastEndColumn, range.startLineNumber, range.startColumn))); // (2) -- Push new text - if (operation.lines) { - for (let j = 0, lenJ = operation.lines.length; j < lenJ; j++) { - if (j !== 0) { - result.push('\n'); - } - result.push(operation.lines[j]); - } + if (operation.text.length > 0) { + result.push(operation.text); } - lastEndLineNumber = operation.range.endLineNumber; - lastEndColumn = operation.range.endColumn; + lastEndLineNumber = range.endLineNumber; + lastEndColumn = range.endColumn; } + const text = result.join(''); + const [eolCount, firstLineLength, lastLineLength] = countEOL(text); + return { sortIndex: 0, identifier: operations[0].identifier, range: entireEditRange, rangeOffset: this.getOffsetAt(entireEditRange.startLineNumber, entireEditRange.startColumn), rangeLength: this.getValueLengthInRange(entireEditRange, EndOfLinePreference.TextDefined), - lines: result.join('').split('\n'), + text: text, + eolCount: eolCount, + firstLineLength: firstLineLength, + lastLineLength: lastLineLength, forceMoveMarkers: forceMoveMarkers, isAutoWhitespaceEdit: false }; @@ -421,41 +444,26 @@ export class PieceTreeTextBuffer implements ITextBuffer { const endLineNumber = op.range.endLineNumber; 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 continue; } - const deletingLinesCnt = endLineNumber - startLineNumber; - 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) { + if (op.text) { // replacement this._pieceTree.delete(op.rangeOffset, op.rangeLength); - this._pieceTree.insert(op.rangeOffset, text, true); + this._pieceTree.insert(op.rangeOffset, op.text, true); } else { // deletion 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); contentChanges.push({ range: contentChangeRange, rangeLength: op.rangeLength, - text: text, + text: op.text, rangeOffset: op.rangeOffset, forceMoveMarkers: op.forceMoveMarkers }); @@ -504,18 +512,16 @@ export class PieceTreeTextBuffer implements ITextBuffer { let resultRange: Range; - if (op.lines && op.lines.length > 0) { + if (op.text.length > 0) { // the operation inserts something - let lineCount = op.lines.length; - let firstLine = op.lines[0]; - let lastLine = op.lines[lineCount - 1]; + const lineCount = op.eolCount + 1; if (lineCount === 1) { // single line insert - resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + firstLine.length); + resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + op.firstLineLength); } else { // 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 { // There is nothing to insert diff --git a/src/vs/editor/common/model/textChange.ts b/src/vs/editor/common/model/textChange.ts new file mode 100644 index 0000000000..d3e241f0dd --- /dev/null +++ b/src/vs/editor/common/model/textChange.ts @@ -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; + } +} diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 939dfb3ce1..f7632eb374 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -37,6 +37,7 @@ import { Color } from 'vs/base/common/color'; import { Constants } from 'vs/base/common/uint'; import { EditorTheme } from 'vs/editor/common/view/viewContext'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { TextChange } from 'vs/editor/common/model/textChange'; function createTextBufferBuilder() { return new PieceTreeTextBufferBuilder(); @@ -367,7 +368,6 @@ export class TextModel extends Disposable implements model.ITextModel { this._onWillDispose.fire(); this._languageRegistryListener.dispose(); this._tokenization.dispose(); - this._undoRedoService.removeElements(this.uri); this._isDisposed = true; super.dispose(); this._isDisposing = false; @@ -711,7 +711,11 @@ export class TextModel extends Disposable implements model.ITextModel { 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; } @@ -1285,19 +1289,39 @@ export class TextModel extends Disposable implements model.ITextModel { 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((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((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 { this._onDidChangeDecorations.beginDeferredEmit(); this._eventEmitter.beginDeferredEmit(); this._isUndoing = isUndoing; this._isRedoing = isRedoing; - let reverseEdits: model.IValidEditOperations[] = []; - for (let i = 0, len = edits.length; i < len; i++) { - reverseEdits[i] = { operations: this.applyEdits(edits[i].operations) }; - } + this.applyEdits(edits, false); this.setEOL(eol); this._overwriteAlternativeVersionId(resultingAlternativeVersionId); - return reverseEdits; } finally { this._isUndoing = 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 { this._onDidChangeDecorations.beginDeferredEmit(); this._eventEmitter.beginDeferredEmit(); - return this._doApplyEdits(this._validateEditOperations(rawOperations)); + const operations = this._validateEditOperations(rawOperations); + return this._doApplyEdits(operations, computeUndoEdits); } finally { this._eventEmitter.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 result = this._buffer.applyEdits(rawOperations, this._options.trimAutoWhitespace); + const result = this._buffer.applyEdits(rawOperations, this._options.trimAutoWhitespace, computeUndoEdits); const newLineCount = this._buffer.getLineCount(); 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 { diff --git a/src/vs/editor/common/model/tokensStore.ts b/src/vs/editor/common/model/tokensStore.ts index d054fff61c..e9afd106e0 100644 --- a/src/vs/editor/common/model/tokensStore.ts +++ b/src/vs/editor/common/model/tokensStore.ts @@ -11,10 +11,18 @@ import { ColorId, FontStyle, LanguageId, MetadataConsts, StandardTokenType, Toke import { writeUInt32BE, readUInt32BE } from 'vs/base/common/buffer'; 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 firstLineLength = 0; let lastLineStart = 0; + let eol: StringEOL = StringEOL.Unknown; for (let i = 0, len = text.length; i < len; i++) { const chr = text.charCodeAt(i); @@ -25,12 +33,16 @@ export function countEOL(text: string): [number, number, number] { eolCount++; if (i + 1 < len && text.charCodeAt(i + 1) === CharCode.LineFeed) { // \r\n... case + eol |= StringEOL.CRLF; i++; // skip \n } else { // \r... case + eol |= StringEOL.Invalid; } lastLineStart = i + 1; } else if (chr === CharCode.LineFeed) { + // \n... case + eol |= StringEOL.LF; if (eolCount === 0) { firstLineLength = i; } @@ -41,7 +53,7 @@ export function countEOL(text: string): [number, number, number] { if (eolCount === 0) { firstLineLength = text.length; } - return [eolCount, firstLineLength, text.length - lastLineStart]; + return [eolCount, firstLineLength, text.length - lastLineStart, eol]; } function getDefaultMetadata(topLevelLanguageId: LanguageId): number { diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 1182bb3955..3b3825c0b4 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1396,6 +1396,15 @@ export interface AuthenticationSession { accountName: string; } +/** + * @internal + */ +export interface AuthenticationSessionsChangeEvent { + added: string[]; + removed: string[]; + changed: string[]; +} + export interface Command { id: string; title: string; diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 2574e13045..f5ea8e56a2 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -3,6 +3,7 @@ * 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 { Disposable, IDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; 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 { IThemeService } from 'vs/platform/theme/common/themeService'; 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 { enabled?: boolean; @@ -35,6 +43,18 @@ function MODEL_ID(resource: URI): string { 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 { public readonly model: ITextModel; @@ -98,13 +118,42 @@ interface IRawConfig { const DEFAULT_EOL = (platform.isLinux || platform.isMacintosh) ? DefaultEndOfLine.LF : DefaultEndOfLine.CRLF; -export class ModelServiceImpl extends Disposable implements IModelService { - public _serviceBrand: undefined; +interface EditStackPastFutureElements { + past: EditStackElement[]; + future: EditStackElement[]; +} - private readonly _configurationService: IConfigurationService; - private readonly _configurationServiceSubscription: IDisposable; - private readonly _resourcePropertiesService: ITextResourcePropertiesService; - private readonly _undoRedoService: IUndoRedoService; +function isEditStackPastFutureElements(undoElements: IPastFutureElements): undoElements is EditStackPastFutureElements { + return (isEditStackElements(undoElements.past) && isEditStackElements(undoElements.future)); +} + +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 = this._register(new Emitter()); public readonly onModelAdded: Event = 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; }>()); public readonly onModelModeChanged: Event<{ model: ITextModel; oldModeId: string; }> = this._onModelModeChanged.event; - private _modelCreationOptionsByLanguageAndResource: { - [languageAndResource: string]: ITextModelCreationOptions; - }; + private _modelCreationOptionsByLanguageAndResource: { [languageAndResource: string]: ITextModelCreationOptions; }; /** * All the models known in the system. */ private readonly _models: { [modelId: string]: ModelData; }; + private readonly _disposedModels: Map; constructor( - @IConfigurationService configurationService: IConfigurationService, - @ITextResourcePropertiesService resourcePropertiesService: ITextResourcePropertiesService, - @IThemeService themeService: IThemeService, - @ILogService logService: ILogService, - @IUndoRedoService undoRedoService: IUndoRedoService + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITextResourcePropertiesService private readonly _resourcePropertiesService: ITextResourcePropertiesService, + @IThemeService private readonly _themeService: IThemeService, + @ILogService private readonly _logService: ILogService, + @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, + @IDialogService private readonly _dialogService: IDialogService, ) { super(); - this._configurationService = configurationService; - this._resourcePropertiesService = resourcePropertiesService; - this._undoRedoService = undoRedoService; - this._models = {}; this._modelCreationOptionsByLanguageAndResource = Object.create(null); + this._models = {}; + this._disposedModels = new Map(); - this._configurationServiceSubscription = this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions()); + this._register(this._configurationService.onDidChangeConfiguration(e => 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 { let tabSize = EDITOR_MODEL_DEFAULTS.tabSize; 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)) { tabSize = parsedTabSize; } @@ -158,7 +205,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { let 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)) { indentSize = parsedIndentSize; } @@ -230,14 +277,14 @@ export class ModelServiceImpl extends Disposable implements IModelService { } private _updateModelOptions(): void { - let oldOptionsByLanguageAndResource = this._modelCreationOptionsByLanguageAndResource; + const oldOptionsByLanguageAndResource = this._modelCreationOptionsByLanguageAndResource; this._modelCreationOptionsByLanguageAndResource = Object.create(null); // 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++) { - let modelId = keys[i]; - let modelData = this._models[modelId]; + const modelId = keys[i]; + const modelData = this._models[modelId]; const language = modelData.model.getLanguageIdentifier().language; const uri = modelData.model.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 private _createModelData(value: string | ITextBufferFactory, languageIdentifier: LanguageIdentifier, resource: URI | undefined, isForSimpleWidget: boolean): ModelData { // create & save the model const options = this.getCreationOptions(languageIdentifier.language, resource, isForSimpleWidget); 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); 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); - let oldRange: Range, newRange: Range; + let oldRange: Range; + let newRange: Range; if (commonSuffix > 0) { oldRange = new Range(commonPrefix + 1, 1, modelLineCount - 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) { return; } - let modelData = this._models[MODEL_ID(model.uri)]; + const modelData = this._models[MODEL_ID(model.uri)]; if (!modelData) { return; } @@ -403,19 +464,69 @@ export class ModelServiceImpl extends Disposable implements IModelService { public destroyModel(resource: URI): void { // 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) { 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(); + + // 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[] { - 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++) { - let modelId = keys[i]; + const modelId = keys[i]; ret.push(this._models[modelId].model); } @@ -423,8 +534,8 @@ export class ModelServiceImpl extends Disposable implements IModelService { } public getModel(resource: URI): ITextModel | null { - let modelId = MODEL_ID(resource); - let modelData = this._models[modelId]; + const modelId = MODEL_ID(resource); + const modelData = this._models[modelId]; if (!modelData) { return null; } @@ -434,8 +545,8 @@ export class ModelServiceImpl extends Disposable implements IModelService { // --- end IModelService private _onWillDispose(model: ITextModel): void { - let modelId = MODEL_ID(model.uri); - let modelData = this._models[modelId]; + const modelId = MODEL_ID(model.uri); + const modelData = this._models[modelId]; delete this._models[modelId]; modelData.dispose(); diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index 71672bed36..4172bec449 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -36,7 +36,7 @@ export namespace InspectTokensNLS { } 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 { diff --git a/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts index dd525ab7e9..1697e1ad81 100644 --- a/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts @@ -22,6 +22,10 @@ interface IEditorLineDecoration { overviewRulerDecorationId: string; } +export interface IEditorNavigationQuickAccessOptions { + canAcceptInBackground?: boolean; +} + /** * A reusable quick access provider for the editor with support * for adding decorations for navigating in the currently active file @@ -29,11 +33,16 @@ interface IEditorLineDecoration { */ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQuickAccessProvider { + constructor(protected options?: IEditorNavigationQuickAccessOptions) { } + //#region Provider methods provide(picker: IQuickPick, token: CancellationToken): IDisposable { const disposables = new DisposableStore(); + // Apply options if any + picker.canAcceptInBackground = !!this.options?.canAcceptInBackground; + // Disable filtering & sorting, we control the results picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false; @@ -71,11 +80,11 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState()); })); - once(token.onCancellationRequested)(() => { + disposables.add(once(token.onCancellationRequested)(() => { if (lastKnownEditorViewState) { editor.restoreViewState(lastKnownEditorViewState); } - }); + })); } // Clean up decorations on dispose @@ -110,10 +119,12 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu */ protected abstract provideWithoutTextEditor(picker: IQuickPick, 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.revealRangeInCenter(options.range, ScrollType.Smooth); - editor.focus(); + if (!options.preserveFocus) { + editor.focus(); + } } protected getModel(editor: IEditor | IDiffEditor): ITextModel | undefined { diff --git a/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts index a38e5f0a52..0f2c45fd57 100644 --- a/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts @@ -6,11 +6,13 @@ import { localize } from 'vs/nls'; import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; 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 { IRange } from 'vs/editor/common/core/range'; import { AbstractEditorNavigationQuickAccessProvider } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; 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 { } @@ -18,6 +20,10 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor static PREFIX = ':'; + constructor() { + super({ canAcceptInBackground: true }); + } + protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { 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(); // Goto line once picked - disposables.add(picker.onDidAccept(() => { + disposables.add(picker.onDidAccept(event => { const [item] = picker.selectedItems; if (item) { if (!this.isValidLineNumber(editor, item.lineNumber)) { 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(); 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; } diff --git a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts index 8008fe6ca2..dfac041741 100644 --- a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts @@ -6,25 +6,26 @@ import { localize } from 'vs/nls'; import { IQuickPick, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; 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 { ITextModel } from 'vs/editor/common/model'; 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 { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { values } from 'vs/base/common/collections'; import { trim, format } from 'vs/base/common/strings'; 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, index: number, score?: FuzzyScore; - range?: { decoration: IRange, selection: IRange }, + range?: { decoration: IRange, selection: IRange } } -export interface IGotoSymbolQuickAccessProviderOptions { +export interface IGotoSymbolQuickAccessProviderOptions extends IEditorNavigationQuickAccessOptions { openSideBySideDirection: () => undefined | 'right' | 'down' } @@ -34,8 +35,8 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit static SCOPE_PREFIX = ':'; static PREFIX_BY_CATEGORY = `${AbstractGotoSymbolQuickAccessProvider.PREFIX}${AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX}`; - constructor(private options?: IGotoSymbolQuickAccessProviderOptions) { - super(); + constructor(protected options?: IGotoSymbolQuickAccessProviderOptions) { + super(assign(options, { canAcceptInBackground: true })); } protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { @@ -72,32 +73,58 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit picker.items = [{ label, index: 0, kind: SymbolKind.String }]; 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 // very early after the model has loaded but before the // language registry is ready. // 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 { + if (DocumentSymbolProviderRegistry.has(model)) { + return true; + } + + let symbolProviderRegistryPromiseResolve: (res: boolean) => void; + const symbolProviderRegistryPromise = new Promise(resolve => symbolProviderRegistryPromiseResolve = resolve); + + // Resolve promise when registry knows model const symbolProviderListener = disposables.add(DocumentSymbolProviderRegistry.onDidChange(() => { if (DocumentSymbolProviderRegistry.has(model)) { 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, token: CancellationToken): IDisposable { const disposables = new DisposableStore(); // Goto symbol once picked - disposables.add(picker.onDidAccept(() => { + disposables.add(picker.onDidAccept(event => { const [item] = picker.selectedItems; 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 picker.busy = true; 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) { return; } @@ -167,7 +194,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return disposables; } - private async getSymbolPicks(symbolsPromise: Promise, filter: string, token: CancellationToken): Promise> { + protected async doGetSymbolPicks(symbolsPromise: Promise, filter: string, token: CancellationToken): Promise> { const symbols = await symbolsPromise; if (token.isCancellationRequested) { return []; @@ -340,7 +367,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return result; } - private async getDocumentSymbols(document: ITextModel, flatten: boolean, token: CancellationToken): Promise { + protected async getDocumentSymbols(document: ITextModel, flatten: boolean, token: CancellationToken): Promise { const model = await OutlineModel.create(document, token); if (token.isCancellationRequested) { return []; diff --git a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts index 93c51d7861..b8fa783737 100644 --- a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts @@ -50,7 +50,8 @@ suite('SmartSelect', () => { setup(() => { 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(); }); diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index c4dee163ba..b39abb179b 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -604,6 +604,12 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate false }, mouseSupport: false, + ariaRole: 'listbox', + ariaProvider: { + getRole: () => 'option', + getSetSize: (_: CompletionItem, _index: number, listLength: number) => listLength, + getPosInSet: (_: CompletionItem, index: number) => index, + }, accessibilityProvider: { getAriaLabel: (item: CompletionItem) => { const textLabel = typeof item.completion.label === 'string' ? item.completion.label : item.completion.label.name; diff --git a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts index d64b412ebd..5c4e420d78 100644 --- a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts +++ b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts @@ -41,7 +41,7 @@ export class GotoLineAction extends EditorAction { super({ id: 'editor.action.gotoLine', label: GoToLineNLS.gotoLineActionLabel, - alias: 'Go to Line...', + alias: 'Go to Line/Column...', precondition: undefined, kbOpts: { kbExpr: EditorContextKeys.focus, diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 445b800053..891eaa7dd5 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -156,7 +156,7 @@ export module StaticServices { 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))); diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 9c674e253f..ae0c191a01 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -1348,7 +1348,7 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.Undo.runEditorCommand(null, editor, null); 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); assert.equal(model.getLineContent(1), 'Hello world'); diff --git a/src/vs/editor/test/common/model/benchmark/operations.benchmark.ts b/src/vs/editor/test/common/model/benchmark/operations.benchmark.ts index d9adc00faf..f77235c0a4 100644 --- a/src/vs/editor/test/common/model/benchmark/operations.benchmark.ts +++ b/src/vs/editor/test/common/model/benchmark/operations.benchmark.ts @@ -54,7 +54,7 @@ for (let fileSize of fileSizes) { fn: (textBuffer) => { // for line model, this loop doesn't reflect the real situation. 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) => { for (const edit of edits) { - textBuffer.applyEdits([edit], false); + textBuffer.applyEdits([edit], false, false); } return textBuffer; }, @@ -91,7 +91,7 @@ for (let fileSize of fileSizes) { }, preCycle: (textBuffer) => { for (const edit of edits) { - textBuffer.applyEdits([edit], false); + textBuffer.applyEdits([edit], false, false); } return textBuffer; }, @@ -121,7 +121,7 @@ for (let fileSize of fileSizes) { }, preCycle: (textBuffer) => { for (const edit of edits) { - textBuffer.applyEdits([edit], false); + textBuffer.applyEdits([edit], false, false); } return textBuffer; }, @@ -134,4 +134,4 @@ for (let fileSize of fileSizes) { editsSuite.run(); } -} \ No newline at end of file +} diff --git a/src/vs/editor/test/common/model/benchmark/searchNReplace.benchmark.ts b/src/vs/editor/test/common/model/benchmark/searchNReplace.benchmark.ts index 8662124f8c..3ea75a7edd 100644 --- a/src/vs/editor/test/common/model/benchmark/searchNReplace.benchmark.ts +++ b/src/vs/editor/test/common/model/benchmark/searchNReplace.benchmark.ts @@ -41,10 +41,10 @@ for (let fileSize of fileSizes) { return textBuffer; }, fn: (textBuffer) => { - textBuffer.applyEdits(edits.slice(0, i), false); + textBuffer.applyEdits(edits.slice(0, i), false, false); } }); } replaceSuite.run(); -} \ No newline at end of file +} diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index 92161da0b1..14ef175098 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -1104,7 +1104,7 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { { range: new Range(3, 1, 3, 6), text: null, }, { range: new Range(2, 1, 3, 1), text: null, }, { range: new Range(3, 6, 3, 6), text: '\nline2' } - ]); + ], true); model.applyEdits(undoEdits); diff --git a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts index de89611906..3582b27608 100644 --- a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts +++ b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts @@ -17,7 +17,7 @@ export function testApplyEditsWithSyncedModels(original: string[], edits: IIdent assertSyncedModels(originalStr, (model, assertMirrorModels) => { // Apply edits & collect inverse edits - let inverseEdits = model.applyEdits(edits); + let inverseEdits = model.applyEdits(edits, true); // Assert edits produced expected result assert.deepEqual(model.getValue(EndOfLinePreference.LF), expectedStr); @@ -25,7 +25,7 @@ export function testApplyEditsWithSyncedModels(original: string[], edits: IIdent assertMirrorModels(); // 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.deepEqual(model.getValue(EndOfLinePreference.LF), originalStr); @@ -36,8 +36,8 @@ export function testApplyEditsWithSyncedModels(original: string[], edits: IIdent identifier: edit.identifier, range: edit.range, text: edit.text, - forceMoveMarkers: edit.forceMoveMarkers, - isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit + forceMoveMarkers: edit.forceMoveMarkers || false, + isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit || false }; }; // Assert the inverse of the inverse edits are the original edits diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts index 682025288b..051939772f 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts @@ -18,7 +18,10 @@ suite('PieceTreeTextBuffer._getInverseEdits', () => { range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), rangeOffset: 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, isAutoWhitespaceEdit: false }; @@ -269,7 +272,10 @@ suite('PieceTreeTextBuffer._toSingleEditOperation', () => { range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), rangeOffset: rangeOffset, 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, isAutoWhitespaceEdit: false }; diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index 352e7b78b8..4c3742b7fe 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -330,7 +330,7 @@ suite('Editor Model - Model', () => { let res = thisModel.applyEdits([ { range: new Range(2, 1, 2, 1), text: 'a' }, { range: new Range(1, 1, 1, 1), text: 'b' }, - ]); + ], true); assert.deepEqual(res[0].range, new Range(2, 1, 2, 2)); assert.deepEqual(res[1].range, new Range(1, 1, 1, 2)); diff --git a/src/vs/editor/test/common/model/modelEditOperation.test.ts b/src/vs/editor/test/common/model/modelEditOperation.test.ts index c25d520ef9..e4a9de03f2 100644 --- a/src/vs/editor/test/common/model/modelEditOperation.test.ts +++ b/src/vs/editor/test/common/model/modelEditOperation.test.ts @@ -50,14 +50,14 @@ suite('Editor Model - Model Edit Operation', () => { function assertSingleEditOp(singleEditOp: IIdentifiedSingleEditOperation, editedLines: string[]) { let editOp = [singleEditOp]; - let inverseEditOp = model.applyEdits(editOp); + let inverseEditOp = model.applyEdits(editOp, true); assert.equal(model.getLineCount(), editedLines.length); for (let i = 0; i < editedLines.length; 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.getLineContent(1), LINE1); @@ -71,8 +71,8 @@ suite('Editor Model - Model Edit Operation', () => { identifier: edit.identifier, range: edit.range, text: edit.text, - forceMoveMarkers: edit.forceMoveMarkers, - isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit + forceMoveMarkers: edit.forceMoveMarkers || false, + isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit || false }; }; assert.deepEqual(originalOp.map(simplifyEdit), editOp.map(simplifyEdit)); diff --git a/src/vs/editor/test/common/model/textChange.test.ts b/src/vs/editor/test/common/model/textChange.test.ts new file mode 100644 index 0000000000..46f23e6ce0 --- /dev/null +++ b/src/vs/editor/test/common/model/textChange.test.ts @@ -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(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; + } + } + } +}); diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index 519f7c4637..9636386948 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -9,10 +9,11 @@ import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { EditOperation } from 'vs/editor/common/core/editOperation'; 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 { DefaultEndOfLine } from 'vs/editor/common/model'; 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 { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -33,7 +34,8 @@ suite('ModelService', () => { configService.setUserConfiguration('files', { 'eol': '\n' }); 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(() => { @@ -307,6 +309,75 @@ suite('ModelService', () => { ]; 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 { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index bbc656d78d..16f6718550 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1552,14 +1552,9 @@ declare namespace monaco.editor { */ 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; - /** - * 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; + text: string; } /** @@ -1907,9 +1902,11 @@ declare namespace monaco.editor { * 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. * @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. * This can have dire consequences on the undo stack! See @pushEOL for the preferred way. diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index c0fe2f7082..15a67d63d3 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -125,6 +125,7 @@ export class MenuId { static readonly TimelineItemContext = new MenuId('TimelineItemContext'); static readonly TimelineTitle = new MenuId('TimelineTitle'); static readonly TimelineTitleContext = new MenuId('TimelineTitleContext'); + static readonly AccountsContext = new MenuId('AccountsContext'); readonly id: number; readonly _debugName: string; diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index eb1b6a6206..41cd03eb84 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -332,7 +332,11 @@ export class ElectronMainService implements IElectronMainService { } async closeWindow(windowId: number | undefined): Promise { - const window = this.windowById(windowId); + this.closeWindowById(windowId, windowId); + } + + async closeWindowById(currentWindowId: number | undefined, targetWindowId?: number | undefined): Promise { + const window = this.windowById(targetWindowId); if (window) { return window.win.close(); } diff --git a/src/vs/platform/electron/node/electron.ts b/src/vs/platform/electron/node/electron.ts index 45a0bc8006..4f7f0c9590 100644 --- a/src/vs/platform/electron/node/electron.ts +++ b/src/vs/platform/electron/node/electron.ts @@ -74,6 +74,7 @@ export interface IElectronService { relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise; reload(options?: { disableExtensions?: boolean }): Promise; closeWindow(): Promise; + closeWindowById(windowId: number): Promise; quit(): Promise; // Development diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index fe3bb37b34..21e05f2d5e 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -151,7 +151,7 @@ export class InstantiationService implements IInstantiationService { graph.lookupOrInsertNode(item); // a weak but working heuristic for cycle checks - if (cycleCount++ > 200) { + if (cycleCount++ > 1000) { throw new CyclicDependencyError(graph); } diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 0d1def31fd..909cf98768 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -533,6 +533,7 @@ export class Menubar { [ minimize, zoom, + __separator__(), switchWindow, ...nativeTabMenuItems, __separator__(), diff --git a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts index 7aed8b1ed3..cadf6434f9 100644 --- a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts @@ -6,7 +6,6 @@ import { localize } from 'vs/nls'; import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; 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 { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle'; 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 { INotificationService } from 'vs/platform/notification/common/notification'; 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 { 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 const mapLabelToCommand = new Map(); - for (const commandPick of distinctCommandPicks) { + for (const commandPick of filteredCommandPicks) { const existingCommandForLabel = mapLabelToCommand.get(commandPick.label); if (existingCommandForLabel) { commandPick.description = commandPick.commandId; @@ -90,7 +84,7 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc } // 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 commandBCounter = this.commandsHistory.peek(commandPickB.commandId); @@ -113,8 +107,8 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc const commandPicks: Array = []; let addSeparator = false; - for (let i = 0; i < distinctCommandPicks.length; i++) { - const commandPick = distinctCommandPicks[i]; + for (let i = 0; i < filteredCommandPicks.length; i++) { + const commandPick = filteredCommandPicks[i]; const keybinding = this.keybindingService.lookupKeybinding(commandPick.commandId); const ariaLabel = keybinding ? localize('commandPickAriaLabelWithKeybinding', "{0}, {1}, commands picker", commandPick.label, keybinding.getAriaLabel()) : @@ -143,13 +137,6 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc // Add to history 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 this.telemetryService.publicLog2('workbenchActionExecuted', { 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; diff --git a/src/vs/platform/quickinput/browser/helpQuickAccess.ts b/src/vs/platform/quickinput/browser/helpQuickAccess.ts index 148e3b9336..530c8da839 100644 --- a/src/vs/platform/quickinput/browser/helpQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/helpQuickAccess.ts @@ -66,6 +66,10 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider { const editorProviders: IHelpQuickAccessPickItem[] = []; 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) { const prefix = helpEntry.prefix || provider.prefix; const label = prefix || '\u2026' /* ... */; diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index 53e56503f4..55ec610520 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -25,7 +25,12 @@ export enum TriggerAction { /** * Update the results of the picker. */ - REFRESH_PICKER + REFRESH_PICKER, + + /** + * Remove the item from the picker. + */ + REMOVE_ITEM } export interface IPickerQuickAccessItem extends IQuickPickItem { @@ -211,6 +216,14 @@ export abstract class PickerQuickAccessProvider(Extensions.Quickaccess); private readonly mapProviderToDescriptor = new Map(); - private lastActivePicker: IQuickPick | undefined = undefined; + private readonly lastAcceptedPickerValues = new Map(); + + private visibleQuickAccess: { + picker: IQuickPick, + descriptor: IQuickAccessProviderDescriptor | undefined, + value: string + } | undefined = undefined; constructor( @IQuickInputService private readonly quickInputService: IQuickInputService, @@ -25,33 +39,131 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon super(); } - show(value = '', options?: IQuickAccessOptions): void { - const disposables = new DisposableStore(); - - // Hide any previous picker if any - this.lastActivePicker?.hide(); + show(value = '', options?: IInternalQuickAccessOptions): void { // Find provider for the value to show 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 // and adjust the filtering to exclude the prefix from filtering + const disposables = new DisposableStore(); const picker = disposables.add(this.quickInputService.createQuickPick()); - picker.placeholder = descriptor?.placeholder; picker.value = value; + this.adjustValueSelection(picker, descriptor, options); + picker.placeholder = descriptor?.placeholder; 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.filterValue = (value: string) => value.substring(descriptor ? descriptor.prefix.length : 0); - // Remember as last active picker and clean up once picker get's disposed - this.lastActivePicker = picker; + // Register listeners + 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, 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, 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(() => { - if (picker === this.lastActivePicker) { - this.lastActivePicker = undefined; + if (visibleQuickAccess === this.visibleQuickAccess) { + 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 // picker has not been closed without picking an item const cts = disposables.add(new CancellationTokenSource()); @@ -64,24 +176,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon disposables.dispose(); }); - // 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); - } - })); - - // 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(); + return cts.token; } private getOrInstantiateProvider(value: string): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] { diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 78b1bb3c54..2cda95a535 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -3,7 +3,7 @@ * 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; 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); } - accept() { - return this.controller.accept(); + accept(keyMods?: IKeyMods) { + return this.controller.accept(keyMods); } back() { diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index b0e03da2a6..365471bf79 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -12,15 +12,15 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; 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. */ quickNavigateConfiguration?: IQuickNavigateConfiguration; + + /** + * Wether to select the second pick item by default instead of the first. + */ + autoFocus?: { autoFocusSecondEntry?: boolean } } export interface IQuickAccessController { @@ -31,8 +31,32 @@ export interface IQuickAccessController { 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 { + /** + * 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. * diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index a4056cc493..3f41fa8e21 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; 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'; export * from 'vs/base/parts/quickinput/common/quickInput'; @@ -84,8 +84,11 @@ export interface IQuickInputService { /** * Accept the selected item. + * + * @param keyMods allows to override the state of key + * modifiers that should be present when invoking. */ - accept(): Promise; + accept(keyMods?: IKeyMods): Promise; /** * Cancels quick input and closes it. diff --git a/src/vs/platform/undoRedo/common/undoRedo.ts b/src/vs/platform/undoRedo/common/undoRedo.ts index 3422c3de81..9defb41ff5 100644 --- a/src/vs/platform/undoRedo/common/undoRedo.ts +++ b/src/vs/platform/undoRedo/common/undoRedo.ts @@ -30,6 +30,13 @@ export interface IWorkspaceUndoRedoElement { split(): IResourceUndoRedoElement[]; } +export type IUndoRedoElement = IResourceUndoRedoElement | IWorkspaceUndoRedoElement; + +export interface IPastFutureElements { + past: IUndoRedoElement[]; + future: IUndoRedoElement[]; +} + export interface IUndoRedoService { _serviceBrand: undefined; @@ -37,12 +44,18 @@ export interface IUndoRedoService { * Add a new element to the `undo` 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. */ - 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`. diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index e6f53068a1..53ce3d9dee 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 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 { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -23,6 +23,7 @@ class ResourceStackElement { public readonly strResource: string; public readonly resources: URI[]; public readonly strResources: string[]; + public isValid: boolean; constructor(actual: IResourceUndoRedoElement) { this.actual = actual; @@ -31,6 +32,11 @@ class ResourceStackElement { this.strResource = uriGetComparisonKey(this.resource); this.resources = [this.resource]; this.strResources = [this.strResource]; + this.isValid = true; + } + + public setValid(isValid: boolean): void { + this.isValid = isValid; } } @@ -39,22 +45,57 @@ const enum RemovedResourceReason { NoParallelUniverses = 1 } +class ResourceReasonPair { + constructor( + public readonly resource: URI, + public readonly reason: RemovedResourceReason + ) { } +} + class RemovedResources { - public readonly set: Set = new Set(); - public readonly reason: [URI[], URI[]] = [[], []]; + private readonly elements = new Map(); + + private _getPath(resource: URI): string { + return resource.scheme === Schemas.file ? resource.fsPath : resource.path; + } public createMessage(): string { - let messages: string[] = []; - if (this.reason[RemovedResourceReason.ExternalRemoval].length > 0) { - const paths = this.reason[RemovedResourceReason.ExternalRemoval].map(uri => uri.scheme === Schemas.file ? uri.fsPath : uri.path); - messages.push(nls.localize('externalRemoval', "The following files have been closed: {0}.", paths.join(', '))); + const externalRemoval: string[] = []; + const noParallelUniverses: string[] = []; + for (const [, element] of this.elements) { + 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); - messages.push(nls.localize('noParallelUniverses', "The following files have been modified in an incompatible way: {0}.", paths.join(', '))); + + let messages: string[] = []; + 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'); } + + 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 { @@ -65,6 +106,7 @@ class WorkspaceStackElement { public readonly resources: URI[]; public readonly strResources: string[]; public removedResources: RemovedResources | null; + public invalidatedResources: RemovedResources | null; constructor(actual: IWorkspaceUndoRedoElement) { this.actual = actual; @@ -72,18 +114,37 @@ class WorkspaceStackElement { this.resources = actual.resources.slice(0); this.strResources = this.resources.map(resource => uriGetComparisonKey(resource)); this.removedResources = null; + this.invalidatedResources = null; } public removeResource(resource: URI, strResource: string, reason: RemovedResourceReason): void { if (!this.removedResources) { this.removedResources = new RemovedResources(); } - if (!this.removedResources.set.has(strResource)) { - this.removedResources.set.add(strResource); - this.removedResources.reason[reason].push(resource); + if (!this.removedResources.has(strResource)) { + this.removedResources.set(strResource, new ResourceReasonPair(resource, reason)); + } + } + + 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; class ResourceEditStack { @@ -110,7 +171,7 @@ export class UndoRedoService implements IUndoRedoService { this._editStacks = new Map(); } - public pushElement(_element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement): void { + public pushElement(_element: IUndoRedoElement): void { const element: StackElement = (_element.type === UndoRedoElementType.Resource ? new ResourceStackElement(_element) : new WorkspaceStackElement(_element)); for (let i = 0, len = element.resources.length; i < len; i++) { const resource = element.resources[i]; @@ -131,11 +192,18 @@ export class UndoRedoService implements IUndoRedoService { } } 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); } } - public getLastElement(resource: URI): IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null { + public getLastElement(resource: URI): IUndoRedoElement | null { const strResource = uriGetComparisonKey(resource); if (this._editStacks.has(strResource)) { const editStack = this._editStacks.get(strResource)!; @@ -150,7 +218,7 @@ export class UndoRedoService implements IUndoRedoService { return null; } - private _splitPastWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: Set | null): void { + private _splitPastWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: RemovedResources | null): void { const individualArr = toRemove.actual.split(); const individualMap = new Map(); for (const _element of individualArr) { @@ -178,7 +246,7 @@ export class UndoRedoService implements IUndoRedoService { } } - private _splitFutureWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: Set | null): void { + private _splitFutureWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: RemovedResources | null): void { const individualArr = toRemove.actual.split(); const individualMap = new Map(); 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 { const strResource = uriGetComparisonKey(resource); if (this._editStacks.has(strResource)) { @@ -257,11 +375,17 @@ export class UndoRedoService implements IUndoRedoService { private _workspaceUndo(resource: URI, element: WorkspaceStackElement): Promise | void { 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()); this._notificationService.info(message); 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! let affectedEditStacks: ResourceEditStack[] = []; @@ -313,6 +437,12 @@ export class UndoRedoService implements IUndoRedoService { } private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement): Promise | void { + if (!element.isValid) { + // invalid element => immediately flush edit stack! + editStack.past = []; + editStack.future = []; + return; + } editStack.past.pop(); editStack.future.push(element); return this._safeInvoke(element, () => element.actual.undo()); @@ -348,11 +478,17 @@ export class UndoRedoService implements IUndoRedoService { private _workspaceRedo(resource: URI, element: WorkspaceStackElement): Promise | void { 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()); this._notificationService.info(message); 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! let affectedEditStacks: ResourceEditStack[] = []; @@ -383,6 +519,12 @@ export class UndoRedoService implements IUndoRedoService { } private _resourceRedo(editStack: ResourceEditStack, element: ResourceStackElement): Promise | void { + if (!element.isValid) { + // invalid element => immediately flush edit stack! + editStack.past = []; + editStack.future = []; + return; + } editStack.future.pop(); editStack.past.push(element); return this._safeInvoke(element, () => element.actual.redo()); diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 8687c03b48..1659494491 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -7,9 +7,9 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; 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 { 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 { Emitter, Event } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -173,20 +173,36 @@ export abstract class AbstractSynchroniser extends Disposable { return !!lastSyncData; } - async getConflictContent(conflictResource: URI): Promise { + async getRemoteSyncResourceHandles(): Promise { + const handles = await this.userDataSyncStoreService.getAllRefs(this.resource); + return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) })); + } + + async getLocalSyncResourceHandles(): Promise { + 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 { + 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; } - async getRemoteContent(ref?: string): Promise { - const refOrLastSyncUserData: string | IRemoteUserData | null = ref || await this.getLastSyncUserData(); - const { content } = await this.getUserData(refOrLastSyncUserData); - return content; - } - - async getLocalBackupContent(ref?: string): Promise { - return this.userDataSyncBackupStoreService.resolveContent(this.resource, ref); - } - async resetLocal(): Promise { try { 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)); } + abstract stop(): Promise; + protected abstract readonly version: number; protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; - abstract stop(): Promise; } export interface IFileSyncPreviewResult { @@ -310,7 +327,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { this.setStatus(SyncStatus.Idle); } - async getConflictContent(conflictResource: URI): Promise { + protected async getConflictContent(conflictResource: URI): Promise { if (isEqual(this.remotePreviewResource, conflictResource) || isEqual(this.localPreviewResource, conflictResource)) { if (this.syncPreviewResultPromise) { const result = await this.syncPreviewResultPromise; diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index b76dc0be2a..f9526e27c9 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -3,7 +3,7 @@ * 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 { IEnvironmentService } from 'vs/platform/environment/common/environment'; 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 { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; 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 { readonly localExtensions: ISyncExtension[]; @@ -120,28 +123,24 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async stop(): Promise { } - async getRemoteContent(ref?: string, fragment?: string): Promise { - const content = await super.getRemoteContent(ref); - if (content !== null && fragment) { - return this.getFragment(content, fragment); - } - return content; + async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { + return [{ resource: joinPath(uri, 'extensions.json') }]; } - async getLocalBackupContent(ref?: string, fragment?: string): Promise { - let content = await super.getLocalBackupContent(ref); - if (content !== null && fragment) { - return this.getFragment(content, fragment); + async resolveContent(uri: URI): Promise { + let content = await super.resolveContent(uri); + if (content) { + return content; } - return content; - } - - private getFragment(content: string, fragment: string): string | null { - const syncData = this.parseSyncData(content); - if (syncData) { - switch (fragment) { - case 'extensions': - return syncData.content; + content = await super.resolveContent(dirname(uri)); + if (content) { + const syncData = this.parseSyncData(content); + if (syncData) { + switch (basename(uri)) { + case 'extensions.json': + const edits = format(syncData.content, undefined, {}); + return applyEdits(syncData.content, edits); + } } } return null; diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 3b2188888f..4588b00618 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -3,11 +3,11 @@ * 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 { Event } from 'vs/base/common/event'; 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 { IStringDictionary } from 'vs/base/common/collections'; 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 { IConfigurationService } from 'vs/platform/configuration/common/configuration'; 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']; @@ -105,28 +107,24 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs async stop(): Promise { } - async getRemoteContent(ref?: string, fragment?: string): Promise { - let content = await super.getRemoteContent(ref); - if (content !== null && fragment) { - return this.getFragment(content, fragment); - } - return content; + async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { + return [{ resource: joinPath(uri, 'globalState.json') }]; } - async getLocalBackupContent(ref?: string, fragment?: string): Promise { - let content = await super.getLocalBackupContent(ref); - if (content !== null && fragment) { - return this.getFragment(content, fragment); + async resolveContent(uri: URI): Promise { + let content = await super.resolveContent(uri); + if (content) { + return content; } - return content; - } - - private getFragment(content: string, fragment: string): string | null { - const syncData = this.parseSyncData(content); - if (syncData) { - switch (fragment) { - case 'globalState': - return syncData.content; + content = await super.resolveContent(dirname(uri)); + if (content) { + const syncData = this.parseSyncData(content); + if (syncData) { + switch (basename(uri)) { + case 'globalState.json': + const edits = format(syncData.content, undefined, {}); + return applyEdits(syncData.content, edits); + } } } return null; diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index b91820c0cf..4e4d2d9cad 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 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 { VSBuffer } from 'vs/base/common/buffer'; 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 { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; 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 { mac?: string; @@ -160,38 +160,36 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return false; } - async getConflictContent(conflictResource: URI): Promise { - const content = await super.getConflictContent(conflictResource); - return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null; + async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { + return [{ resource: joinPath(uri, 'keybindings.json'), comparableResource: this.file }]; } - async getRemoteContent(ref?: string, fragment?: string): Promise { - const content = await super.getRemoteContent(ref); - if (content !== null && fragment) { - return this.getFragment(content, fragment); + async resolveContent(uri: URI): Promise { + if (isEqual(this.remotePreviewResource, uri)) { + return this.getConflictContent(uri); } - return content; - } - - async getLocalBackupContent(ref?: string, fragment?: string): Promise { - let content = await super.getLocalBackupContent(ref); - if (content !== null && fragment) { - return this.getFragment(content, fragment); + let content = await super.resolveContent(uri); + if (content) { + return content; } - return content; - } - - private getFragment(content: string, fragment: string): string | null { - const syncData = this.parseSyncData(content); - if (syncData) { - switch (fragment) { - case 'keybindings': - return this.getKeybindingsContentFromSyncContent(syncData.content); + content = await super.resolveContent(dirname(uri)); + if (content) { + const syncData = this.parseSyncData(content); + if (syncData) { + switch (basename(uri)) { + case 'keybindings.json': + return this.getKeybindingsContentFromSyncContent(syncData.content); + } } } return null; } + protected async getConflictContent(conflictResource: URI): Promise { + const content = await super.getConflictContent(conflictResource); + return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null; + } + protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { try { const result = await this.getPreview(remoteUserData, lastSyncUserData); diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 93d7fef82d..68cc805028 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 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 { parse } from 'vs/base/common/json'; import { localize } from 'vs/nls'; @@ -20,7 +20,7 @@ import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; 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 { settings: string; @@ -173,7 +173,35 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { return false; } - async getConflictContent(conflictResource: URI): Promise { + async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { + return [{ resource: joinPath(uri, 'settings.json'), comparableResource: this.file }]; + } + + async resolveContent(uri: URI): Promise { + 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 { let content = await super.getConflictContent(conflictResource); if (content !== null) { const settingsSyncContent = this.parseSettingsSyncContent(content); @@ -188,36 +216,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { return content; } - async getRemoteContent(ref?: string, fragment?: string): Promise { - let content = await super.getRemoteContent(ref); - if (content !== null && fragment) { - return this.getFragment(content, fragment); - } - return content; - } - - async getLocalBackupContent(ref?: string, fragment?: string): Promise { - 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 { if (this.status === SyncStatus.HasConflicts && (isEqual(this.localPreviewResource, conflict) || isEqual(this.remotePreviewResource, conflict)) diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index 65fc303f2b..2cbbf6e558 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -3,7 +3,7 @@ * 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 { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; 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 { IStringDictionary } from 'vs/base/common/collections'; 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 { merge } from 'vs/platform/userDataSync/common/snippetsMerge'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; @@ -148,8 +148,46 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD this.setStatus(SyncStatus.Idle); } - async getConflictContent(conflictResource: URI): Promise { - if (isEqualOrParent(conflictResource.with({ scheme: this.syncFolder.scheme }), this.snippetsPreviewFolder) && this.syncPreviewResultPromise) { + async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { + 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 { + 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 { + if (this.syncPreviewResultPromise) { const result = await this.syncPreviewResultPromise; const key = relativePath(this.snippetsPreviewFolder, conflictResource.with({ scheme: this.snippetsPreviewFolder.scheme }))!; if (conflictResource.scheme === this.snippetsPreviewFolder.scheme) { @@ -162,37 +200,6 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD return null; } - async getRemoteContent(ref?: string, fragment?: string): Promise { - const content = await super.getRemoteContent(ref); - if (content !== null && fragment) { - return this.getFragment(content, fragment); - } - return content; - } - - async getLocalBackupContent(ref?: string, fragment?: string): Promise { - 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 { const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0]; if (this.status === SyncStatus.HasConflicts && conflict) { diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 60c25cbed0..d1ffd4643f 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -18,7 +18,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; 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 { IProductService } from 'vs/platform/product/common/productService'; import { distinct } from 'vs/base/common/arrays'; @@ -243,6 +243,11 @@ export const enum SyncStatus { HasConflicts = 'hasConflicts', } +export interface ISyncResourceHandle { + created: number; + uri: URI; +} + export type Conflict = { remote: URI, local: URI }; export interface IUserDataSynchroniser { @@ -263,11 +268,12 @@ export interface IUserDataSynchroniser { hasLocalData(): Promise; resetLocal(): Promise; - getConflictContent(conflictResource: URI): Promise; + resolveContent(resource: URI): Promise; acceptConflict(conflictResource: URI, content: string): Promise; - getRemoteContent(ref?: string, fragment?: string): Promise; - getLocalBackupContent(ref?: string, fragment?: string): Promise; + getRemoteSyncResourceHandles(): Promise; + getLocalSyncResourceHandles(): Promise; + getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>; } //#endregion @@ -315,6 +321,10 @@ export interface IUserDataSyncService { isFirstTimeSyncWithMerge(): Promise; resolveContent(resource: URI): Promise; acceptConflict(conflictResource: URI, content: string): Promise; + + getLocalSyncResourceHandles(resource: SyncResource): Promise; + getRemoteSyncResourceHandles(resource: SyncResource): Promise; + getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>; } export const IUserDataAutoSyncService = createDecorator('IUserDataAutoSyncService'); @@ -347,25 +357,6 @@ export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync'; export const CONTEXT_SYNC_STATE = new RawContextKey('syncStatus', SyncStatus.Uninitialized); export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey('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 function getSyncResourceFromLocalPreview(localPreview: URI, environmentService: IEnvironmentService): SyncResource | undefined { if (localPreview.scheme === USER_DATA_SYNC_SCHEME) { diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 439f7e0545..3c7fae47bf 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -5,7 +5,7 @@ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; 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 { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; @@ -28,14 +28,17 @@ export class UserDataSyncChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { 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 'sync': return this.service.sync(); case 'stop': this.service.stop(); return Promise.resolve(); case 'reset': return this.service.reset(); case 'resetLocal': return this.service.resetLocal(); - case 'resolveContent': return this.service.resolveContent(URI.revive(args[0])); 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'); } @@ -98,38 +101,3 @@ export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService { } } - -export class UserDataSyncStoreServiceChannel implements IServerChannel { - - constructor(private readonly service: IUserDataSyncStoreService) { } - - listen(_: unknown, event: string): Event { - throw new Error(`Event not found: ${event}`); - } - - call(context: any, command: string, args?: any): Promise { - 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 { - throw new Error(`Event not found: ${event}`); - } - - call(context: any, command: string, args?: any): Promise { - 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'); - } -} diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 57a9858e90..881934db37 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -3,7 +3,7 @@ * 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -188,25 +188,27 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } async resolveContent(resource: URI): Promise { - const result = resolveBackupSyncResource(resource); - if (result) { - const synchronizer = this.synchronisers.filter(s => s.resource === result.resource)[0]; - 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) { + for (const synchroniser of this.synchronisers) { + const content = await synchroniser.resolveContent(resource); + if (content) { return content; } } - return null; } + getRemoteSyncResourceHandles(resource: SyncResource): Promise { + return this.getSynchroniser(resource).getRemoteSyncResourceHandles(); + } + + getLocalSyncResourceHandles(resource: SyncResource): Promise { + return this.getSynchroniser(resource).getLocalSyncResourceHandles(); + } + + getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { + return this.getSynchroniser(resource).getAssociatedResources(syncResourceHandle); + } + async isFirstTimeSyncWithMerge(): Promise { await this.checkEnablement(); if (!await this.userDataSyncStoreService.manifest()) { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 50ee5ad39c..b38cc8494e 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -40,6 +40,26 @@ declare module 'vscode' { 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 { /** * 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 * within a session has changed. */ - readonly onDidChangeSessions: Event; + readonly onDidChangeSessions: Event; /** * 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 * that have had session data change. */ - export const onDidChangeSessions: Event; + export const onDidChangeSessions: Event<{ [providerId: string]: AuthenticationSessionsChangeEvent }>; } //#endregion @@ -1269,17 +1289,11 @@ declare module 'vscode' { //#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 * 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 { /** @@ -1290,7 +1304,7 @@ declare module 'vscode' { * * @return Thenable signaling that the save has completed. */ - save(document: CustomDocument, cancellation: CancellationToken): Thenable; + save(document: CustomDocument, cancellation: CancellationToken): Thenable; /** * Save the existing resource at a new path. @@ -1300,7 +1314,7 @@ declare module 'vscode' { * * @return Thenable signaling that the save has completed. */ - saveAs(document: CustomDocument, targetResource: Uri): Thenable; + saveAs(document: CustomDocument, targetResource: Uri): Thenable; /** * 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. */ - applyEdits(document: CustomDocument, edits: readonly EditType[]): Thenable; + applyEdits(document: CustomDocument, edits: readonly EditType[]): Thenable; /** * Undo a set of edits. @@ -1329,7 +1343,7 @@ declare module 'vscode' { * * @return Thenable signaling that the change has completed. */ - undoEdits(document: CustomDocument, edits: readonly EditType[]): Thenable; + undoEdits(document: CustomDocument, edits: readonly EditType[]): Thenable; /** * Revert the file to its last saved state. @@ -1339,7 +1353,7 @@ declare module 'vscode' { * * @return Thenable signaling that the change has completed. */ - revert(document: CustomDocument, edits: CustomDocumentRevert): Thenable; + revert(document: CustomDocument, edits: CustomDocumentRevert): Thenable; /** * 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 * than cancelling it to ensure that VS Code has some valid backup. */ - backup(document: CustomDocument, cancellation: CancellationToken): Thenable; + backup(document: CustomDocument, cancellation: CancellationToken): Thenable; } /** - * 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 { /** * Document the edit is for. */ - readonly document: CustomDocument; + readonly document: CustomDocument; /** * 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; @@ -1403,13 +1420,19 @@ declare module 'vscode' { /** * Represents a custom document used by a `CustomEditorProvider`. * - * Custom documents are only used within a given `CustomEditorProvider`. The lifecycle of a - * `CustomDocument` is managed by VS Code. When no more references remain to a given `CustomDocument`, - * then it is disposed of. + * All custom documents must subclass `CustomDocument`. Custom documents are only used within a given + * `CustomEditorProvider`. The lifecycle of a `CustomDocument` is managed by VS Code. When no more references + * 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 { + class CustomDocument { + /** + * @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. */ @@ -1426,12 +1449,17 @@ declare module 'vscode' { readonly onDidDispose: Event; /** - * 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; - // 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; } /** @@ -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 * based documents, use [`WebviewTextEditorProvider`](#WebviewTextEditorProvider) instead. */ - export interface CustomEditorProvider { + export interface CustomEditorProvider { + /** * 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 * 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. * - * @return The capabilities of the resolved document. + * @return The custom document. */ - resolveCustomDocument(document: CustomDocument, token: CancellationToken): Thenable; // TODO: rename to open? + openCustomDocument(uri: Uri, token: CancellationToken): Thenable>; /** * 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 - * existing editor using this `CustomTextEditorProvider`. + * This is called when a user first opens a resource for a `CustomEditorProvider`, or if they reopen an + * existing editor using this `CustomEditorProvider`. * * 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, @@ -1475,14 +1504,14 @@ declare module 'vscode' { * * @return Thenable indicating that the webview editor has been resolved. */ - resolveCustomEditor(document: CustomDocument, webviewPanel: WebviewPanel, token: CancellationToken): Thenable; + resolveCustomEditor(document: CustomDocument, webviewPanel: WebviewPanel, token: CancellationToken): Thenable; /** * Defines the editing capability of a custom webview document. * * When not provided, the document is considered readonly. */ - readonly editingDelegate?: CustomEditorEditingDelegate; + readonly editingDelegate?: CustomEditorEditingDelegate; } /** @@ -1496,6 +1525,7 @@ declare module 'vscode' { * For binary files or more specialized use cases, see [CustomEditorProvider](#CustomEditorProvider). */ export interface CustomTextEditorProvider { + /** * 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. */ moveCustomTextEditor?(newDocument: TextDocument, existingWebviewPanel: WebviewPanel, token: CancellationToken): Thenable; - - // TODO: handlesMove?: boolean; } namespace window { @@ -1540,14 +1568,16 @@ declare module 'vscode' { * @param viewType Type of the webview editor provider. This should match the `viewType` from the * `package.json` contributions. * @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. */ export function registerCustomEditorProvider( viewType: string, provider: CustomEditorProvider | CustomTextEditorProvider, - webviewOptions?: WebviewPanelOptions, // TODO: move this onto provider? + options?: { + readonly webviewOptions?: WebviewPanelOptions; + } ): Disposable; } @@ -1636,7 +1666,16 @@ declare module 'vscode' { export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; export interface NotebookCellMetadata { + /** + * Controls if the content of a cell is editable or not. + */ editable: boolean; + + /** + * Controls if the cell is executable. + * This metadata is ignored for markdown cell. + */ + runnable: boolean; } export interface NotebookCell { @@ -1650,7 +1689,23 @@ declare module 'vscode' { } export interface NotebookDocumentMetadata { + /** + * Controls if users can add or delete cells + * Default to true + */ 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 { @@ -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) */ - 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 { @@ -1874,17 +1929,10 @@ declare module 'vscode' { export interface Timeline { 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 before: string; - readonly after?: string - }; - - /** - * A flag which indicates whether there are more items that weren't returned. - */ - readonly more?: boolean; + readonly cursor: string | undefined; } /** @@ -1895,19 +1943,15 @@ declare module 'vscode' { 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; /** - * 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; - - /** - * The maximum number or the ending cursor of timeline items that should be returned. - */ - limit?: number | { cursor: string }; + limit?: number | { timestamp: number; id?: string }; } export interface TimelineProvider { diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 7beafd7adb..d23f1d323d 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -3,7 +3,7 @@ * 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 nls from 'vs/nls'; 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 { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; 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(); + private _sessionIds: string[] = []; -export class MainThreadAuthenticationProvider { constructor( private readonly _proxy: ExtHostAuthenticationShape, 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> { return (await this._proxy.$getSessions(this.id)).map(session => { @@ -30,6 +158,24 @@ export class MainThreadAuthenticationProvider { }); } + async updateSessionItems(): Promise { + 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 { return this._proxy.$login(this.id, scopes).then(session => { return { @@ -40,8 +186,14 @@ export class MainThreadAuthenticationProvider { }); } - logout(accountId: string): Promise { - return this._proxy.$logout(this.id, accountId); + logout(sessionId: string): Promise { + 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); } - $registerAuthenticationProvider(id: string, displayName: string): void { - const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName); + async $registerAuthenticationProvider(id: string, displayName: string): Promise { + 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); } @@ -68,8 +222,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this.authenticationService.unregisterAuthenticationProvider(id); } - $onDidChangeSessions(id: string): void { - this.authenticationService.sessionsUpdate(id); + $onDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void { + this.authenticationService.sessionsUpdate(id, event); } async $getSessionsPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 4814526ddb..8ebd1e8e42 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -8,7 +8,7 @@ import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IEx import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; 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 { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; 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 { + async $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise { let controller = this._notebookProviders.get(viewType); if (controller) { @@ -135,6 +135,14 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } } + async $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata): Promise { + let controller = this._notebookProviders.get(viewType); + + if (controller) { + controller.updateNotebookCellMetadata(resource, handle, metadata); + } + } + async resolveNotebook(viewType: string, uri: URI): Promise { let handle = await this._proxy.$resolveNotebook(viewType, uri); return handle; @@ -228,25 +236,25 @@ export class MainThreadNotebookController implements IMainNotebookController { document?.textModel.updateLanguages(languages); } - updateNotebookMetadata(resource: UriComponents, metadata: NotebookDocumentMetadata | undefined) { + updateNotebookMetadata(resource: UriComponents, metadata: NotebookDocumentMetadata) { let document = this._mapping.get(URI.from(resource).toString()); 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 { let document = this._mapping.get(URI.from(resource).toString()); 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 { let cell = await this._proxy.$createEmptyCell(this._viewType, uri, index, language, type); 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; } @@ -263,12 +271,8 @@ export class MainThreadNotebookController implements IMainNotebookController { return false; } - async executeNotebookActiveCell(uri: URI): Promise { - let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); - - if (mainthreadNotebook && mainthreadNotebook.textModel.activeCell) { - return this._proxy.$executeNotebook(this._viewType, uri, mainthreadNotebook.textModel.activeCell.handle); - } + async executeNotebookCell(uri: URI, handle: number): Promise { + return this._proxy.$executeNotebook(this._viewType, uri, handle); } async destoryNotebookDocument(notebook: INotebookTextModel): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index e3aed4e51a..3c30ff5889 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -11,6 +11,7 @@ import { Disposable, DisposableStore, dispose, IDisposable, IReference } from 'v import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; import { isWeb } from 'vs/base/common/platform'; +import { isEqual } from 'vs/base/common/resources'; import { escape } from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; 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 { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; 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 { CustomTextEditorModel } from 'vs/workbench/contrib/customEditor/common/customTextEditorModel'; import { WebviewExtensionDescription, WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview'; @@ -70,6 +72,10 @@ class WebviewInputStore { public get size(): number { return this._handlesToInputs.size; } + + [Symbol.iterator](): Iterator { + return this._handlesToInputs.values(); + } } class WebviewViewTypeTransformer { @@ -374,7 +380,10 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma const model = modelType === ModelType.Text ? 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); } @@ -548,7 +557,6 @@ namespace HotExitState { export type State = typeof Allowed | typeof NotAllowed | Pending; } -const customDocumentFileScheme = 'custom'; class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy { @@ -562,17 +570,19 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod proxy: extHostProtocol.ExtHostWebviewsShape, viewType: string, resource: URI, + getEditors: () => CustomEditorInput[], cancellation: CancellationToken, ) { 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( private readonly _proxy: extHostProtocol.ExtHostWebviewsShape, private readonly _viewType: string, - private readonly _realResource: URI, + private readonly _editorResource: URI, private readonly _editable: boolean, + private readonly _getEditors: () => CustomEditorInput[], @IWorkingCopyService workingCopyService: IWorkingCopyService, @ILabelService private readonly _labelService: ILabelService, @IFileService private readonly _fileService: IFileService, @@ -587,9 +597,9 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod dispose() { 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(); } @@ -598,15 +608,15 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod public get resource() { // Make sure each custom editor has a unique resource for backup and edits return URI.from({ - scheme: customDocumentFileScheme, + scheme: Schemas.vscodeCustomEditor, authority: this._viewType, - path: this._realResource.path, - query: JSON.stringify(this._realResource.toJSON()) + path: this._editorResource.path, + query: JSON.stringify(this._editorResource.toJSON()), }); } public get name() { - return basename(this._labelService.getUriLabel(this._realResource)); + return basename(this._labelService.getUriLabel(this._editorResource)); } public get capabilities(): WorkingCopyCapabilities { @@ -645,7 +655,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod this._undoService.pushElement({ type: UndoRedoElementType.Resource, - resource: this._realResource, + resource: this._editorResource, label: label ?? localize('defaultEditLabel', "Edit"), undo: () => this.undo(), redo: () => this.redo(), @@ -663,11 +673,18 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } const undoneEdit = this._edits[this._currentEditIndex]; - await this._proxy.$undo(this._realResource, this.viewType, undoneEdit); - this.change(() => { --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 { @@ -681,10 +698,10 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } const redoneEdit = this._edits[this._currentEditIndex + 1]; - await this._proxy.$redo(this._realResource, this.viewType, redoneEdit); this.change(() => { ++this._currentEditIndex; }); + await this._proxy.$redo(this._editorResource, this.viewType, redoneEdit, this.getEditState()); } private spliceEdits(editToInsert?: number) { @@ -696,7 +713,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod : this._edits.splice(start, toRemove); 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); } - 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._currentEditIndex = this._savePoint; this.spliceEdits(); @@ -739,7 +756,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod if (!this._editable) { 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._savePoint = this._currentEditIndex; }); @@ -748,7 +765,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod public async saveAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise { if (this._editable) { - await this._proxy.$onSaveAs(this._realResource, this.viewType, targetResource); + await this._proxy.$onSaveAs(this._editorResource, this.viewType, targetResource); this.change(() => { this._savePoint = this._currentEditIndex; }); @@ -761,9 +778,25 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } public async backup(): Promise { - 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 = { meta: { 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( createCancelablePromise(token => - this._proxy.$backup(this._realResource.toJSON(), this.viewType, token))); + this._proxy.$backup(this._editorResource.toJSON(), this.viewType, token))); this._hotExitState = pendingState; try { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 0169c60731..fc4f065c59 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -202,7 +202,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I login(providerId: string, scopes: string[]): Thenable { return extHostAuthentication.login(extension, providerId, scopes); }, - get onDidChangeSessions(): Event { + get onDidChangeSessions(): Event<{ [providerId: string]: vscode.AuthenticationSessionsChangeEvent }> { 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*/ } })); }, withProgress(options: vscode.ProgressOptions, task: (progress: vscode.Progress<{ message?: string; worked?: number }>, token: vscode.CancellationToken) => Thenable) { - if (typeof options.location === 'object') { - checkProposedApiEnabled(extension); - } return extHostProgress.withProgress(extension, options, task); }, createOutputChannel(name: string): vscode.OutputChannel { @@ -586,9 +583,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => { 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); - return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options); + return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options?.webviewOptions); }, registerDecorationProvider(provider: vscode.DecorationProvider) { checkProposedApiEnabled(extension); @@ -1048,12 +1045,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CallHierarchyItem: extHostTypes.CallHierarchyItem, DebugConsoleMode: extHostTypes.DebugConsoleMode, Decoration: extHostTypes.Decoration, - WebviewContentState: extHostTypes.WebviewContentState, UIKind: UIKind, ColorThemeKind: extHostTypes.ColorThemeKind, TimelineItem: extHostTypes.TimelineItem, CellKind: extHostTypes.CellKind, - CellOutputKind: extHostTypes.CellOutputKind + CellOutputKind: extHostTypes.CellOutputKind, + CustomDocument: extHostTypes.CustomDocument, }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 70559ac721..bab104e524 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -160,7 +160,7 @@ export interface MainThreadCommentsShape extends IDisposable { export interface MainThreadAuthenticationShape extends IDisposable { $registerAuthenticationProvider(id: string, displayName: 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; $loginPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise; } @@ -626,6 +626,12 @@ export interface WebviewPanelViewStateData { }; } +export interface CustomDocumentEditState { + readonly allEdits: readonly number[]; + readonly currentIndex: number; + readonly saveIndex: number; +} + export interface ExtHostWebviewsShape { $onMessage(handle: WebviewPanelHandle, message: any): void; $onMissingCsp(handle: WebviewPanelHandle, extensionId: string): void; @@ -638,9 +644,9 @@ export interface ExtHostWebviewsShape { $createWebviewCustomEditorDocument(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<{ editable: boolean }>; $disposeWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise; - $undo(resource: UriComponents, viewType: string, editId: number): Promise; - $redo(resource: UriComponents, viewType: string, editId: number): Promise; - $revert(resource: UriComponents, viewType: string, changes: { undoneEdits: number[], redoneEdits: number[] }): Promise; + $undo(resource: UriComponents, viewType: string, editId: number, state: CustomDocumentEditState): Promise; + $redo(resource: UriComponents, viewType: string, editId: number, state: CustomDocumentEditState): Promise; + $revert(resource: UriComponents, viewType: string, changes: { undoneEdits: number[], redoneEdits: number[] }, state: CustomDocumentEditState): Promise; $disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void; $onSave(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; @@ -691,7 +697,8 @@ export interface MainThreadNotebookShape extends IDisposable { $unregisterNotebookRenderer(handle: number): Promise; $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; - $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata | undefined): Promise; + $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise; + $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata | undefined): Promise; $spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): Promise; $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise; $postMessage(handle: number, value: any): Promise; diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 2d7179f46b..333c113264 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -17,8 +17,8 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _onDidChangeAuthenticationProviders = new Emitter(); readonly onDidChangeAuthenticationProviders: Event = this._onDidChangeAuthenticationProviders.event; - private _onDidChangeSessions = new Emitter(); - readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; + private _onDidChangeSessions = new Emitter<{ [providerId: string]: vscode.AuthenticationSessionsChangeEvent }>(); + readonly onDidChangeSessions: Event<{ [providerId: string]: vscode.AuthenticationSessionsChangeEvent }> = this._onDidChangeSessions.event; constructor(mainContext: IMainContext) { this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication); @@ -85,9 +85,9 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { this._authenticationProviders.set(provider.id, provider); - const listener = provider.onDidChangeSessions(_ => { - this._proxy.$onDidChangeSessions(provider.id); - this._onDidChangeSessions.fire([provider.id]); + const listener = provider.onDidChangeSessions(e => { + this._proxy.$onDidChangeSessions(provider.id, e); + this._onDidChangeSessions.fire({ [provider.id]: e }); }); this._proxy.$registerAuthenticationProvider(provider.id, provider.displayName); diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index bf4d9982f2..c486a93b5b 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -16,6 +16,12 @@ import { INotebookDisplayOrder, ITransformedDisplayOutputDto, IOrderedMimeType, import { ISplice } from 'vs/base/common/sequence'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +const notebookDocumentMetadataDefaults: vscode.NotebookDocumentMetadata = { + editable: true, + cellEditable: true, + cellRunnable: true +}; + export class ExtHostCell implements vscode.NotebookCell { public source: string[]; @@ -27,13 +33,16 @@ export class ExtHostCell implements vscode.NotebookCell { private _outputMapping = new Set(); constructor( + private viewType: string, + private documentUri: URI, readonly handle: number, readonly uri: URI, private _content: string, public cellKind: CellKind, public language: string, 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._outputs = outputs; @@ -62,6 +71,20 @@ export class ExtHostCell implements vscode.NotebookCell { 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 { if (this._textDocument && this._initalVersion !== this._textDocument?.version) { 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); } - private _metadata: vscode.NotebookDocumentMetadata | undefined = undefined; + private _metadata: vscode.NotebookDocumentMetadata | undefined = notebookDocumentMetadataDefaults; get metadata() { return this._metadata; } set metadata(newMetadata: vscode.NotebookDocumentMetadata | undefined) { - this._metadata = newMetadata; + this._metadata = newMetadata || notebookDocumentMetadataDefaults; this._proxy.$updateNotebookMetadata(this.viewType, this.uri, this._metadata); } @@ -201,6 +224,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo language: cell.language, cellKind: cell.cellKind, outputs: outputs, + metadata: cell.metadata, isDirty: false }; }); @@ -346,7 +370,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook onDidReceiveMessage: vscode.Event = this._onDidReceiveMessage.event; constructor( - viewType: string, + private readonly viewType: string, readonly id: string, public uri: URI, 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 { const handle = ExtHostNotebookEditor._cellhandlePool++; 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; } @@ -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 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!); let allDocuments = this._documentsAndEditors.allDocuments(); diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 2669cec22f..561a66258f 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4,16 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce, equals } from 'vs/base/common/arrays'; +import { escapeCodicons } from 'vs/base/common/codicons'; import { illegalArgument } from 'vs/base/common/errors'; +import { Emitter } from 'vs/base/common/event'; import { IRelativePattern } from 'vs/base/common/glob'; import { isMarkdownString } from 'vs/base/common/htmlContent'; import { startsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; -import type * as vscode from 'vscode'; import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files'; 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 { ///@ts-ignore @@ -2538,13 +2541,6 @@ export class Decoration { bubble?: boolean; } -export enum WebviewContentState { - Readonly = 1, - Unchanged = 2, - Dirty = 3, -} - - //#region Theming @es5ClassCompat @@ -2584,3 +2580,84 @@ export class TimelineItem implements vscode.TimelineItem { } //#endregion Timeline + +//#region Custom Editors + +interface EditState { + readonly allEdits: readonly number[]; + readonly currentIndex: number; + readonly saveIndex: number; +} + +export class CustomDocument implements vscode.CustomDocument { + + + readonly #edits = new Cache('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(); + 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 diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index cbf34566ab..9e3dfa27a9 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -18,9 +18,8 @@ import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; -import { Cache } from './cache'; -import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewExtensionDescription, WebviewPanelHandle, WebviewPanelViewStateData } from './extHost.protocol'; -import { Disposable as VSCodeDisposable } from './extHostTypes'; +import * as extHostProtocol from './extHost.protocol'; +import * as extHostTypes from './extHostTypes'; type IconPath = URI | { light: URI, dark: URI }; @@ -33,8 +32,8 @@ export class ExtHostWebview implements vscode.Webview { public readonly onDidReceiveMessage: Event = this._onMessageEmitter.event; constructor( - private readonly _handle: WebviewPanelHandle, - private readonly _proxy: MainThreadWebviewsShape, + private readonly _handle: extHostProtocol.WebviewPanelHandle, + private readonly _proxy: extHostProtocol.MainThreadWebviewsShape, private _options: vscode.WebviewOptions, private readonly _initData: WebviewInitData, private readonly _workspace: IExtHostWorkspace | undefined, @@ -99,8 +98,8 @@ export class ExtHostWebview implements vscode.Webview { export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPanel { - private readonly _handle: WebviewPanelHandle; - private readonly _proxy: MainThreadWebviewsShape; + private readonly _handle: extHostProtocol.WebviewPanelHandle; + private readonly _proxy: extHostProtocol.MainThreadWebviewsShape; private readonly _viewType: string; private _title: string; private _iconPath?: IconPath; @@ -121,8 +120,8 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa public readonly onDidChangeViewState = this.#onDidChangeViewState.event; constructor( - handle: WebviewPanelHandle, - proxy: MainThreadWebviewsShape, + handle: extHostProtocol.WebviewPanelHandle, + proxy: extHostProtocol.MainThreadWebviewsShape, viewType: string, title: string, viewColumn: vscode.ViewColumn | undefined, @@ -246,114 +245,14 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa } } -class CustomDocument extends Disposable implements vscode.CustomDocument { - - public static create( - viewType: string, - uri: vscode.Uri, - editingDelegate: vscode.CustomEditorEditingDelegate | undefined - ) { - return Object.seal(new CustomDocument(viewType, uri, editingDelegate)); - } - - // Explicitly initialize all properties as we seal the object after creation! - - readonly #_edits = new Cache('edits'); - - readonly #viewType: string; - readonly #uri: vscode.Uri; - readonly #editingDelegate: vscode.CustomEditorEditingDelegate | undefined; - - private constructor( - viewType: string, - uri: vscode.Uri, - editingDelegate: vscode.CustomEditorEditingDelegate | undefined, - ) { - super(); - this.#viewType = viewType; - this.#uri = uri; - this.#editingDelegate = editingDelegate; - } - - dispose() { - this.#onDidDispose.fire(); - super.dispose(); - } - - //#region Public API - - public get viewType(): string { return this.#viewType; } - - public get uri(): vscode.Uri { return this.#uri; } - - #onDidDispose = this._register(new Emitter()); - public readonly onDidDispose = this.#onDidDispose.event; - - public userData: unknown = undefined; - - //#endregion - - //#region Internal - - /** @internal*/ async _revert(changes: { undoneEdits: number[], redoneEdits: number[] }) { - const editing = this.getEditingDelegate(); - const undoneEdits = changes.undoneEdits.map(id => this.#_edits.get(id, 0)); - const appliedEdits = changes.redoneEdits.map(id => this.#_edits.get(id, 0)); - return editing.revert(this, { undoneEdits, appliedEdits }); - } - - /** @internal*/ _undo(editId: number) { - const editing = this.getEditingDelegate(); - const edit = this.#_edits.get(editId, 0); - return editing.undoEdits(this, [edit]); - } - - /** @internal*/ _redo(editId: number) { - const editing = this.getEditingDelegate(); - const edit = this.#_edits.get(editId, 0); - return editing.applyEdits(this, [edit]); - } - - /** @internal*/ _save(cancellation: CancellationToken) { - return this.getEditingDelegate().save(this, cancellation); - } - - /** @internal*/ _saveAs(target: vscode.Uri) { - return this.getEditingDelegate().saveAs(this, target); - } - - /** @internal*/ _backup(cancellation: CancellationToken) { - return this.getEditingDelegate().backup(this, cancellation); - } - - /** @internal*/ _disposeEdits(editIds: number[]) { - for (const editId of editIds) { - this.#_edits.delete(editId); - } - } - - /** @internal*/ _pushEdit(edit: unknown): number { - return this.#_edits.add([edit]); - } - - //#endregion - - private getEditingDelegate(): vscode.CustomEditorEditingDelegate { - if (!this.#editingDelegate) { - throw new Error('Document is not editable'); - } - return this.#editingDelegate; - } -} - class WebviewDocumentStore { - private readonly _documents = new Map(); + private readonly _documents = new Map(); - public get(viewType: string, resource: vscode.Uri): CustomDocument | undefined { + public get(viewType: string, resource: vscode.Uri): extHostTypes.CustomDocument | undefined { return this._documents.get(this.key(viewType, resource)); } - public add(document: CustomDocument) { + public add(document: extHostTypes.CustomDocument) { const key = this.key(document.viewType, document.uri); if (this._documents.has(key)) { throw new Error(`Document already exists for viewType:${document.viewType} resource:${document.uri}`); @@ -361,7 +260,7 @@ class WebviewDocumentStore { this._documents.set(key, document); } - public delete(document: CustomDocument) { + public delete(document: extHostTypes.CustomDocument) { const key = this.key(document.viewType, document.uri); this._documents.delete(key); } @@ -406,18 +305,18 @@ class EditorProviderStore { throw new Error(`Provider for viewType:${viewType} already registered`); } this._providers.set(viewType, { type, extension, provider } as ProviderEntry); - return new VSCodeDisposable(() => this._providers.delete(viewType)); + return new extHostTypes.Disposable(() => this._providers.delete(viewType)); } } -export class ExtHostWebviews implements ExtHostWebviewsShape { +export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { - private static newHandle(): WebviewPanelHandle { + private static newHandle(): extHostProtocol.WebviewPanelHandle { return generateUuid(); } - private readonly _proxy: MainThreadWebviewsShape; - private readonly _webviewPanels = new Map(); + private readonly _proxy: extHostProtocol.MainThreadWebviewsShape; + private readonly _webviewPanels = new Map(); private readonly _serializers = new Map { + return new extHostTypes.Disposable(() => { this._serializers.delete(viewType); this._proxy.$unregisterSerializer(viewType); }); @@ -497,21 +396,21 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { if (provider.editingDelegate) { disposables.add(provider.editingDelegate.onDidEdit(e => { const document = e.document; - const editId = (document as CustomDocument)._pushEdit(e.edit); + const editId = (document as extHostTypes.CustomDocument)._addEdit(e.edit); this._proxy.$onDidEdit(document.uri, document.viewType, editId, e.label); })); } } - return VSCodeDisposable.from( + return extHostTypes.Disposable.from( disposables, - new VSCodeDisposable(() => { + new extHostTypes.Disposable(() => { this._proxy.$unregisterEditorProvider(viewType); })); } public $onMessage( - handle: WebviewPanelHandle, + handle: extHostProtocol.WebviewPanelHandle, message: any ): void { const panel = this.getWebviewPanel(handle); @@ -521,13 +420,13 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } public $onMissingCsp( - _handle: WebviewPanelHandle, + _handle: extHostProtocol.WebviewPanelHandle, extensionId: string ): void { this._logService.warn(`${extensionId} created a webview without a content security policy: https://aka.ms/vscode-webview-missing-csp`); } - public $onDidChangeWebviewPanelViewStates(newStates: WebviewPanelViewStateData): void { + public $onDidChangeWebviewPanelViewStates(newStates: extHostProtocol.WebviewPanelViewStateData): void { const handles = Object.keys(newStates); // Notify webviews of state changes in the following order: // - Non-visible @@ -560,7 +459,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } } - async $onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Promise { + async $onDidDisposeWebviewPanel(handle: extHostProtocol.WebviewPanelHandle): Promise { const panel = this.getWebviewPanel(handle); if (panel) { panel.dispose(); @@ -569,7 +468,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } async $deserializeWebviewPanel( - webviewHandle: WebviewPanelHandle, + webviewHandle: extHostProtocol.WebviewPanelHandle, viewType: string, title: string, state: any, @@ -599,9 +498,8 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } const revivedResource = URI.revive(resource); - const document = CustomDocument.create(viewType, revivedResource, entry.provider.editingDelegate); - await entry.provider.resolveCustomDocument(document, cancellation); - this._documents.add(document); + const document = await entry.provider.openCustomDocument(revivedResource, cancellation); + this._documents.add(document as extHostTypes.CustomDocument); return { editable: !!entry.provider.editingDelegate, }; @@ -620,12 +518,12 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { const revivedResource = URI.revive(resource); const document = this.getCustomDocument(viewType, revivedResource); this._documents.delete(document); - document.dispose(); + document._dispose(); } async $resolveWebviewEditor( resource: UriComponents, - handle: WebviewPanelHandle, + handle: extHostProtocol.WebviewPanelHandle, viewType: string, title: string, position: EditorViewColumn, @@ -686,50 +584,73 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { await (entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor!(document, webview, CancellationToken.None); } - async $undo(resourceComponents: UriComponents, viewType: string, editId: number): Promise { + async $undo(resourceComponents: UriComponents, viewType: string, editId: number, state: extHostProtocol.CustomDocumentEditState): Promise { + const delegate = this.getEditingDelegate(viewType); const document = this.getCustomDocument(viewType, resourceComponents); - return document._undo(editId); + document._updateEditState(state); + return delegate.undoEdits(document, [document._getEdit(editId)]); } - async $redo(resourceComponents: UriComponents, viewType: string, editId: number): Promise { + async $redo(resourceComponents: UriComponents, viewType: string, editId: number, state: extHostProtocol.CustomDocumentEditState): Promise { + const delegate = this.getEditingDelegate(viewType); const document = this.getCustomDocument(viewType, resourceComponents); - return document._redo(editId); + document._updateEditState(state); + return delegate.applyEdits(document, [document._getEdit(editId)]); } - async $revert(resourceComponents: UriComponents, viewType: string, changes: { undoneEdits: number[], redoneEdits: number[] }): Promise { + async $revert(resourceComponents: UriComponents, viewType: string, changes: { undoneEdits: number[], redoneEdits: number[] }, state: extHostProtocol.CustomDocumentEditState): Promise { + const delegate = this.getEditingDelegate(viewType); const document = this.getCustomDocument(viewType, resourceComponents); - return document._revert(changes); + const undoneEdits = changes.undoneEdits.map(id => document._getEdit(id)); + const appliedEdits = changes.redoneEdits.map(id => document._getEdit(id)); + document._updateEditState(state); + return delegate.revert(document, { undoneEdits, appliedEdits }); } async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { + const delegate = this.getEditingDelegate(viewType); const document = this.getCustomDocument(viewType, resourceComponents); - return document._save(cancellation); + return delegate.save(document, cancellation); } async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents): Promise { + const delegate = this.getEditingDelegate(viewType); const document = this.getCustomDocument(viewType, resourceComponents); - return document._saveAs(URI.revive(targetResource)); + return delegate.saveAs(document, URI.revive(targetResource)); } async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { + const delegate = this.getEditingDelegate(viewType); const document = this.getCustomDocument(viewType, resourceComponents); - return document._backup(cancellation); + return delegate.backup(document, cancellation); } - private getWebviewPanel(handle: WebviewPanelHandle): ExtHostWebviewEditor | undefined { + private getWebviewPanel(handle: extHostProtocol.WebviewPanelHandle): ExtHostWebviewEditor | undefined { return this._webviewPanels.get(handle); } - private getCustomDocument(viewType: string, resource: UriComponents): CustomDocument { + private getCustomDocument(viewType: string, resource: UriComponents): extHostTypes.CustomDocument { const document = this._documents.get(viewType, URI.revive(resource)); if (!document) { throw new Error('No webview editor custom document found'); } return document; } + + private getEditingDelegate(viewType: string): vscode.CustomEditorEditingDelegate { + const entry = this._editorProviders.get(viewType); + if (!entry) { + throw new Error(`No provider found for '${viewType}'`); + } + const delegate = (entry.provider as vscode.CustomEditorProvider).editingDelegate; + if (!delegate) { + throw new Error(`Provider for ${viewType}' does not support editing`); + } + return delegate; + } } -function toExtensionData(extension: IExtensionDescription): WebviewExtensionDescription { +function toExtensionData(extension: IExtensionDescription): extHostProtocol.WebviewExtensionDescription { return { id: extension.identifier, location: extension.extensionLocation }; } diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 7b29e04253..553b5a7fae 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -41,7 +41,7 @@ class InspectContextKeysAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const disposables = new DisposableStore(); const stylesheet = createStyleSheet(); @@ -85,8 +85,6 @@ class InspectContextKeysAction extends Action { dispose(disposables); }, null, disposables); - - return Promise.resolve(); } } diff --git a/src/vs/workbench/browser/actions/helpActions.ts b/src/vs/workbench/browser/actions/helpActions.ts index 43f94c2656..661c6294b4 100644 --- a/src/vs/workbench/browser/actions/helpActions.ts +++ b/src/vs/workbench/browser/actions/helpActions.ts @@ -31,13 +31,11 @@ class KeybindingsReferenceAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const url = isLinux ? this.productService.keyboardShortcutsUrlLinux : isMacintosh ? this.productService.keyboardShortcutsUrlMac : this.productService.keyboardShortcutsUrlWin; if (url) { this.openerService.open(URI.parse(url)); } - - return Promise.resolve(); } } @@ -56,12 +54,10 @@ class OpenDocumentationUrlAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { if (this.productService.documentationUrl) { this.openerService.open(URI.parse(this.productService.documentationUrl)); } - - return Promise.resolve(); } } @@ -80,12 +76,10 @@ class OpenIntroductoryVideosUrlAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { if (this.productService.introductoryVideosUrl) { this.openerService.open(URI.parse(this.productService.introductoryVideosUrl)); } - - return Promise.resolve(); } } @@ -104,12 +98,10 @@ class OpenTipsAndTricksUrlAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { if (this.productService.tipsAndTricksUrl) { this.openerService.open(URI.parse(this.productService.tipsAndTricksUrl)); } - - return Promise.resolve(); } } @@ -151,12 +143,10 @@ class OpenTwitterUrlAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { if (this.productService.twitterUrl) { this.openerService.open(URI.parse(this.productService.twitterUrl)); } - - return Promise.resolve(); } } @@ -175,12 +165,10 @@ class OpenRequestFeatureUrlAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { if (this.productService.requestFeatureUrl) { this.openerService.open(URI.parse(this.productService.requestFeatureUrl)); } - - return Promise.resolve(); } } @@ -199,7 +187,7 @@ class OpenLicenseUrlAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { if (this.productService.licenseUrl) { if (language) { const queryArgChar = this.productService.licenseUrl.indexOf('?') > 0 ? '&' : '?'; @@ -208,8 +196,6 @@ class OpenLicenseUrlAction extends Action { this.openerService.open(URI.parse(this.productService.licenseUrl)); } } - - return Promise.resolve(); } } @@ -228,7 +214,7 @@ class OpenPrivacyStatementUrlAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { if (this.productService.privacyStatementUrl) { if (language) { const queryArgChar = this.productService.privacyStatementUrl.indexOf('?') > 0 ? '&' : '?'; @@ -237,8 +223,6 @@ class OpenPrivacyStatementUrlAction extends Action { this.openerService.open(URI.parse(this.productService.privacyStatementUrl)); } } - - return Promise.resolve(); } } diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 41ddf35618..48e57c87e2 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -645,9 +645,8 @@ export class IncreaseViewSizeAction extends BaseResizeViewAction { super(id, label, layoutService); } - run(): Promise { + async run(): Promise { this.resizePart(BaseResizeViewAction.RESIZE_INCREMENT); - return Promise.resolve(true); } } @@ -665,9 +664,8 @@ export class DecreaseViewSizeAction extends BaseResizeViewAction { super(id, label, layoutService); } - run(): Promise { + async run(): Promise { this.resizePart(-BaseResizeViewAction.RESIZE_INCREMENT); - return Promise.resolve(true); } } diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 88ed68e246..9b87c5a53d 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -36,7 +36,7 @@ export const inRecentFilesPickerContextKey = 'inRecentFilesPicker'; abstract class BaseOpenRecentAction extends Action { - private removeFromRecentlyOpened: IQuickInputButton = { + private readonly removeFromRecentlyOpened: IQuickInputButton = { iconClass: 'codicon-close', tooltip: nls.localize('remove', "Remove from Recently Opened") }; @@ -124,7 +124,7 @@ abstract class BaseOpenRecentAction extends Action { const pick = await this.quickInputService.pick(picks, { contextKey: inRecentFilesPickerContextKey, activeItem: [...workspacePicks, ...filePicks][autoFocusSecondEntry ? 1 : 0], - placeHolder: isMacintosh ? nls.localize('openRecentPlaceHolderMac', "Select to open (hold Cmd-key to open in new window)") : nls.localize('openRecentPlaceHolder', "Select to open (hold Ctrl-key to open in new window)"), + placeHolder: isMacintosh ? nls.localize('openRecentPlaceHolderMac', "Select to open (hold Cmd-key to force new window or Alt-key for same window)") : nls.localize('openRecentPlaceHolder', "Select to open (hold Ctrl-key to force new window or Alt-key for same window)"), matchOnDescription: true, onKeyMods: mods => keyMods = mods, quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, @@ -135,7 +135,7 @@ abstract class BaseOpenRecentAction extends Action { }); if (pick) { - return this.hostService.openWindow([pick.openable], { forceNewWindow: keyMods?.ctrlCmd }); + return this.hostService.openWindow([pick.openable], { forceNewWindow: keyMods?.ctrlCmd, forceReuseWindow: keyMods?.alt }); } } } diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index 8c3cfb4368..5e2558db6a 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -112,11 +112,10 @@ export class CloseWorkspaceAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { this.notificationService.info(nls.localize('noWorkspaceOpened', "There is currently no workspace opened in this instance to close.")); - - return Promise.resolve(undefined); + return; } return this.hostService.openWindow({ forceReuseWindow: true, remoteAuthority: this.environmentService.configuration.remoteAuthority }); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 320e6dabe3..41bc74f597 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -645,15 +645,19 @@ export class CompositeDragAndDropObserver extends Disposable { } return this._register(disposableStore); } - registerDraggable(element: HTMLElement, type: ViewType, id: string, callbacks: ICompositeDragAndDropObserverCallbacks): IDisposable { + + registerDraggable(element: HTMLElement, draggedItemProvider: () => { type: ViewType, id: string }, callbacks: ICompositeDragAndDropObserverCallbacks): IDisposable { element.draggable = true; const disposableStore = new DisposableStore(); disposableStore.add(addDisposableListener(element, EventType.DRAG_START, e => { + const { id, type } = draggedItemProvider(); this.writeDragData(id, type); this._onDragStart.fire({ eventData: e, dragAndDropData: this.readDragData(type)! }); })); disposableStore.add(new DragAndDropObserver(element, { onDragEnd: e => { + const { id, type } = draggedItemProvider(); + const data = this.readDragData(type); if (data && data.getData().id === id) { this.transferData.clearData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype); @@ -666,6 +670,7 @@ export class CompositeDragAndDropObserver extends Disposable { this._onDragEnd.fire({ eventData: e, dragAndDropData: data! }); }, onDragEnter: e => { + if (callbacks.onDragEnter) { const data = this.readDragData('composite') || this.readDragData('view'); if (!data) { diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 588c870578..fee64a726a 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -364,8 +364,8 @@ class ResourceLabelWidget extends IconLabel { } setResource(label: IResourceLabelProps, options: IResourceLabelOptions = Object.create(null)): void { - /*const resource = toResource(this.label); {{SQL CARBON EDIT}} we don't want to special case untitled files - const isMasterDetail = this.label?.resource && !URI.isUri(this.label.resource); + /*const resource = toResource(label); {{SQL CARBON EDIT}} we don't want to special case untitled files + const isMasterDetail = label?.resource && !URI.isUri(label.resource); if (!isMasterDetail && resource?.scheme === Schemas.untitled) { // Untitled labels are very dynamic because they may change diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 0d5c7a83b8..8db7d9293d 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -424,11 +424,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (!this.state.fullscreen && !this.state.maximized && (activeBorder || inactiveBorder)) { windowBorder = true; - // If one color is missing, just fallback to the other one - const borderColor = this.state.hasFocus - ? activeBorder ?? inactiveBorder - : inactiveBorder ?? activeBorder; - this.container.style.setProperty('--window-border-color', borderColor ? borderColor.toString() : 'transparent'); + // If the inactive color is missing, fallback to the active one + const borderColor = this.state.hasFocus ? activeBorder : inactiveBorder ?? activeBorder; + this.container.style.setProperty('--window-border-color', borderColor?.toString() ?? 'transparent'); } if (windowBorder === this.state.windowBorder) { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 39fc5eead3..9eaa156638 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -134,6 +134,61 @@ export class ToggleViewletAction extends Action { } } +export class AccountsActionViewItem extends ActivityActionViewItem { + constructor( + action: ActivityAction, + colors: (theme: IColorTheme) => ICompositeBarColors, + @IThemeService themeService: IThemeService, + @IContextMenuService protected contextMenuService: IContextMenuService, + @IMenuService protected menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + super(action, { draggable: false, colors, icon: true }, themeService); + } + + render(container: HTMLElement): void { + super.render(container); + + // Context menus are triggered on mouse down so that an item can be picked + // and executed with releasing the mouse over it + + this._register(DOM.addDisposableListener(this.container, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => { + DOM.EventHelper.stop(e, true); + this.showContextMenu(); + })); + + this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, (e: KeyboardEvent) => { + let event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + DOM.EventHelper.stop(e, true); + this.showContextMenu(); + } + })); + + this._register(DOM.addDisposableListener(this.container, TouchEventType.Tap, (e: GestureEvent) => { + DOM.EventHelper.stop(e, true); + this.showContextMenu(); + })); + } + + private showContextMenu(): void { + const accountsActions: IAction[] = []; + const accountsMenu = this.menuService.createMenu(MenuId.AccountsContext, this.contextKeyService); + const actionsDisposable = createAndFillInActionBarActions(accountsMenu, undefined, { primary: [], secondary: accountsActions }); + + const containerPosition = DOM.getDomNodePagePosition(this.container); + const location = { x: containerPosition.left + containerPosition.width / 2, y: containerPosition.top }; + this.contextMenuService.showContextMenu({ + getAnchor: () => location, + getActions: () => accountsActions, + onHide: () => { + accountsMenu.dispose(); + dispose(actionsDisposable); + } + }); + } +} + export class GlobalActivityActionViewItem extends ActivityActionViewItem { constructor( @@ -231,7 +286,7 @@ class SwitchSideBarViewAction extends Action { const activeViewlet = this.viewletService.getActiveViewlet(); if (!activeViewlet) { - return Promise.resolve(); + return; } let targetViewletId: string | undefined; for (let i = 0; i < pinnedViewletIds.length; i++) { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 501ecea840..5bd47e8dbc 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -9,7 +9,7 @@ import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/acti import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; import { Registry } from 'vs/platform/registry/common/platform'; import { Part } from 'vs/workbench/browser/part'; -import { GlobalActivityActionViewItem, ViewletActivityAction, ToggleViewletAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewletActivityAction } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; +import { GlobalActivityActionViewItem, ViewletActivityAction, ToggleViewletAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewletActivityAction, AccountsActionViewItem } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IWorkbenchLayoutService, Parts, Position as SideBarPosition } from 'vs/workbench/services/layout/browser/layoutService'; @@ -354,7 +354,17 @@ export class ActivitybarPart extends Part implements IActivityBarService { private createGlobalActivityActionBar(container: HTMLElement): void { this.globalActivityActionBar = this._register(new ActionBar(container, { - actionViewItemProvider: action => this.instantiationService.createInstance(GlobalActivityActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)), + actionViewItemProvider: action => { + if (action.id === 'workbench.actions.manage') { + return this.instantiationService.createInstance(GlobalActivityActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)); + } + + if (action.id === 'workbench.actions.accounts') { + return this.instantiationService.createInstance(AccountsActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)); + } + + throw new Error(`No view item for action '${action.id}'`); + }, orientation: ActionsOrientation.VERTICAL, ariaLabel: nls.localize('manage', "Manage"), animated: false @@ -366,6 +376,13 @@ export class ActivitybarPart extends Part implements IActivityBarService { cssClass: 'codicon-settings-gear' }); + const profileAction = new ActivityAction({ + id: 'workbench.actions.accounts', + name: nls.localize('accounts', "Accounts"), + cssClass: 'codicon-account' + }); + + this.globalActivityActionBar.push(profileAction); this.globalActivityActionBar.push(this.globalActivityAction); } diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index 187a1a4358..31378b8fc4 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -9,8 +9,8 @@ margin-bottom: 4px; } -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item::before, -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item::after { +.monaco-workbench .activitybar > .content .composite-bar > .monaco-action-bar .action-item::before, +.monaco-workbench .activitybar > .content .composite-bar > .monaco-action-bar .action-item::after { position: absolute; content: ''; width: 48px; @@ -23,25 +23,32 @@ transition-delay: 100ms; } -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item::before { +.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item::before { margin-top: -3px; margin-bottom: 1px; } -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item::after { +/* Override top element since it would be cut off */ +.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item:first-of-type::before { + margin-top: 0px; + margin-bottom: 0px; +} + +.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item::after { margin-top: 1px; margin-bottom: -3px; } -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.top::before, -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.top::after, -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.bottom::before, -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.bottom::after { +.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item.top::before, +.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item.top::after, +.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item.bottom::before, +.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item.bottom::after { transition-delay: 0s; } -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.top::before, -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.bottom::after { +.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item.top::before, +.monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item.bottom::after, +.monaco-workbench .activitybar > .content.dragged-over > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { opacity: 1; } diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index f83c8222d1..d2c4866b03 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -12,12 +12,12 @@ import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ActionBar, ActionsOrientation, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { CompositeActionViewItem, CompositeOverflowActivityAction, ICompositeActivity, CompositeOverflowActivityActionViewItem, ActivityAction, ICompositeBar, ICompositeBarColors } from 'vs/workbench/browser/parts/compositeBarActions'; -import { Dimension, $, addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom'; +import { Dimension, $, addDisposableListener, EventType, EventHelper, toggleClass, isAncestor } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Widget } from 'vs/base/browser/ui/widget'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; +import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { Emitter } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; @@ -187,7 +187,6 @@ export class CompositeBar extends Widget implements ICompositeBar { constructor( items: ICompositeBarItem[], private options: ICompositeBarOptions, - @IThemeService private readonly themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextMenuService private readonly contextMenuService: IContextMenuService ) { @@ -215,8 +214,6 @@ export class CompositeBar extends Widget implements ICompositeBar { create(parent: HTMLElement): HTMLElement { const actionBarDiv = parent.appendChild($('.composite-bar')); - const excessDiv = parent.appendChild($('.composite-bar-excess')); - this.compositeSwitcherBar = this._register(new ActionBar(actionBarDiv, { actionViewItemProvider: (action: IAction) => { if (action instanceof CompositeOverflowActivityAction) { @@ -242,23 +239,26 @@ export class CompositeBar extends Widget implements ICompositeBar { this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e))); // Register a drop target on the whole bar to prevent forbidden feedback - this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(parent, {})); + this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(parent, { + onDragOver: (e: IDraggedCompositeData) => { + // don't add feedback if this is over the composite bar actions + if (e.eventData.target && isAncestor(e.eventData.target as HTMLElement, actionBarDiv)) { + toggleClass(parent, 'dragged-over', false); + return; + } - // Allow to drop at the end to move composites to the end - this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(excessDiv, { - onDragEnter: (e: IDraggedCompositeData) => { const pinnedItems = this.getPinnedComposites(); - const validDropTarget = this.options.dndHandler.onDragEnter(e.dragAndDropData, pinnedItems[pinnedItems.length - 1].id, e.eventData); - this.updateFromDragging(excessDiv, validDropTarget); + const validDropTarget = this.options.dndHandler.onDragOver(e.dragAndDropData, pinnedItems[pinnedItems.length - 1].id, e.eventData); + toggleClass(parent, 'dragged-over', validDropTarget); }, onDragLeave: (e: IDraggedCompositeData) => { - this.updateFromDragging(excessDiv, false); + toggleClass(parent, 'dragged-over', false); }, onDrop: (e: IDraggedCompositeData) => { const pinnedItems = this.getPinnedComposites(); this.options.dndHandler.drop(e.dragAndDropData, pinnedItems[pinnedItems.length - 1].id, e.eventData, false); - this.updateFromDragging(excessDiv, false); + toggleClass(parent, 'dragged-over', false); } })); @@ -364,13 +364,6 @@ export class CompositeBar extends Widget implements ICompositeBar { } } - private updateFromDragging(element: HTMLElement, isDragging: boolean): void { - const theme = this.themeService.getColorTheme(); - const dragBackground = this.options.colors(theme).dragAndDropBackground; - - element.style.backgroundColor = isDragging && dragBackground ? dragBackground.toString() : ''; - } - private resetActiveComposite(compositeId: string) { const defaultCompositeId = this.options.getDefaultCompositeId(); diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 9cbc6575c1..4b5b7f9389 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -518,7 +518,7 @@ export class CompositeActionViewItem extends ActivityActionViewItem { let insertDropBefore: boolean | undefined = undefined; // Allow to drag - this._register(CompositeDragAndDropObserver.INSTANCE.registerDraggable(this.container, 'composite', this.activity.id, { + this._register(CompositeDragAndDropObserver.INSTANCE.registerDraggable(this.container, () => { return { type: 'composite', id: this.activity.id }; }, { onDragOver: e => { const isValidMove = e.dragAndDropData.getData().id !== this.activity.id && this.dndHandler.onDragOver(e.dragAndDropData, this.activity.id, e.eventData); insertDropBefore = this.updateFromDragging(container, isValidMove, e.eventData); @@ -533,6 +533,7 @@ export class CompositeActionViewItem extends ActivityActionViewItem { }, onDrop: e => { + dom.EventHelper.stop(e.eventData, true); this.dndHandler.drop(e.dragAndDropData, this.activity.id, e.eventData, !!insertDropBefore); insertDropBefore = this.updateFromDragging(container, false, e.eventData); }, @@ -554,9 +555,7 @@ export class CompositeActionViewItem extends ActivityActionViewItem { // Activate on drag over to reveal targets [this.badge, this.label].forEach(b => this._register(new DelayedDragHandler(b, () => { - if (!(this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype) || - this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) && - !this.getAction().checked) { + if (!this.getAction().checked) { this.getAction().run(); } }))); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index ccd97a6975..33894861a9 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -292,11 +292,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { localize('closeGroupAction', "Close"), 'codicon-close', true, - () => { - this.accessor.removeGroup(this); - - return Promise.resolve(true); - })); + async () => this.accessor.removeGroup(this))); const keybinding = this.keybindingService.lookupKeybinding(removeGroupAction.id); containerToolbar.push(removeGroupAction, { icon: true, label: false, keybinding: keybinding ? keybinding.getLabel() : undefined }); @@ -807,7 +803,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Guard against invalid inputs if (!editor) { - return Promise.resolve(null); + return null; } // Editor opening event allows for prevention @@ -822,13 +818,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return withUndefinedAsNull(await this.doOpenEditor(editor, options)); } - private doOpenEditor(editor: EditorInput, options?: EditorOptions): Promise { + private async doOpenEditor(editor: EditorInput, options?: EditorOptions): Promise { // Guard against invalid inputs. Disposed inputs // should never open because they emit no events // e.g. to indicate dirty changes. if (editor.isDisposed()) { - return Promise.resolve(undefined); + return undefined; // {{SQL CARBON EDIT}} strict-null-checks } // Determine options diff --git a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts index 93855ab14f..85e2bca2ff 100644 --- a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts +++ b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/editorquickaccess'; import { localize } from 'vs/nls'; -import { IQuickPickSeparator, quickPickItemScorerAccessor, IQuickPickItemWithResource } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickPickSeparator, quickPickItemScorerAccessor, IQuickPickItemWithResource, IQuickPick } from 'vs/platform/quickinput/common/quickInput'; import { PickerQuickAccessProvider, IPickerQuickAccessItem, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { IEditorGroupsService, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorsOrder, IEditorIdentifier, toResource, SideBySideEditor } from 'vs/workbench/common/editor'; @@ -13,12 +13,31 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; -import { prepareQuery, scoreItem, compareItemsByScore } from 'vs/base/common/fuzzyScorer'; +import { prepareQuery, scoreItem, compareItemsByScore, ScorerCache } from 'vs/base/common/fuzzyScorer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable } from 'vs/base/common/lifecycle'; interface IEditorQuickPickItem extends IQuickPickItemWithResource, IEditorIdentifier, IPickerQuickAccessItem { } export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessProvider { + private readonly pickState = new class { + + scorerCache: ScorerCache = Object.create(null); + isQuickNavigating: boolean | undefined = undefined; + + reset(isQuickNavigating: boolean): void { + + // Caches + if (!isQuickNavigating) { + this.scorerCache = Object.create(null); + } + + // Other + this.isQuickNavigating = isQuickNavigating; + } + }; + constructor( prefix: string, @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, @@ -29,9 +48,17 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro super(prefix, { canAcceptInBackground: true }); } + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + + // Reset the pick state for this run + this.pickState.reset(!!picker.quickNavigate); + + // Start picker + return super.provide(picker, token); + } + protected getPicks(filter: string): Array { const query = prepareQuery(filter); - const scorerCache = Object.create(null); // Filtering const filteredEditorEntries = this.doGetEditorPickItems().filter(entry => { @@ -40,7 +67,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro } // Score on label and description - const itemScore = scoreItem(entry, query, true, quickPickItemScorerAccessor, scorerCache); + const itemScore = scoreItem(entry, query, true, quickPickItemScorerAccessor, this.pickState.scorerCache); if (!itemScore.score) { return false; } @@ -59,7 +86,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro return groups.indexOf(entryA.groupId) - groups.indexOf(entryB.groupId); // older groups first } - return compareItemsByScore(entryA, entryB, query, true, quickPickItemScorerAccessor, scorerCache); + return compareItemsByScore(entryA, entryB, query, true, quickPickItemScorerAccessor, this.pickState.scorerCache); }); } @@ -99,17 +126,30 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro description: editor.getDescription(), iconClasses: getIconClasses(this.modelService, this.modeService, resource), italic: !this.editorGroupService.getGroup(groupId)?.isPinned(editor), - buttons: [ - { - iconClass: isDirty ? 'dirty-editor codicon-circle-filled' : 'codicon-close', - tooltip: localize('closeEditor', "Close Editor"), - alwaysVisible: isDirty + buttons: (() => { + if (this.pickState.isQuickNavigating) { + return undefined; // no actions when quick navigating } - ], - trigger: async () => { - await this.editorGroupService.getGroup(groupId)?.closeEditor(editor, { preserveFocus: true }); - return TriggerAction.REFRESH_PICKER; + return [ + { + iconClass: isDirty ? 'dirty-editor codicon-circle-filled' : 'codicon-close', + tooltip: localize('closeEditor', "Close Editor"), + alwaysVisible: isDirty + } + ]; + })(), + trigger: async () => { + const group = this.editorGroupService.getGroup(groupId); + if (group) { + await group.closeEditor(editor, { preserveFocus: true }); + + if (!group.isOpened(editor)) { + return TriggerAction.REMOVE_ITEM; + } + } + + return TriggerAction.NO_ACTION; }, accept: (keyMods, event) => this.editorGroupService.getGroup(groupId)?.openEditor(editor, { preserveFocus: event.inBackground }), }; diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 6383e84f8f..40368b13d8 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -457,7 +457,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, - tooltip: nls.localize('gotoLine', "Go to Line"), + tooltip: nls.localize('gotoLine', "Go to Line/Column"), command: 'workbench.action.gotoLine' }; diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 11d331a1f4..8a69e3b408 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -110,7 +110,7 @@ export class NoTabsTitleControl extends TitleControl { } } } else { - // @rebornix + // TODO@rebornix // gesture tap should open the quick open // editorGroupView will focus on the editor again when there are mouse/pointer/touch down events // we need to wait a bit as `GesureEvent.Tap` is generated from `touchstart` and then `touchend` evnets, which are not an atom event. diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 89f3bf1c9c..14acbd6c8f 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -207,13 +207,13 @@ export class TabsTitleControl extends TitleControl { EventHelper.stop(e); - this.group.openEditor(this.editorService.createEditorInput({ - forceUntitled: true, - options: { + this.group.openEditor( + this.editorService.createEditorInput({ forceUntitled: true }), + { pinned: true, // untitled is always pinned index: this.group.count // always at the end } - })); + ); })); }); diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 5d18ea3a84..650f482421 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -107,6 +107,11 @@ margin-left: 9px; } +.monaco-workbench .part.panel > .composite.title> .panel-switcher-container > .monaco-action-bar .action-item:last-of-type::after { + margin-right: -10px; + margin-left: 8px; +} + .monaco-workbench .part.panel > .composite.title> .panel-switcher-container > .monaco-action-bar .action-item.right::before, .monaco-workbench .part.panel > .composite.title> .panel-switcher-container > .monaco-action-bar .action-item.left::after, .monaco-workbench .part.panel > .composite.title> .panel-switcher-container > .monaco-action-bar .action-item.left::before, @@ -115,7 +120,8 @@ } .monaco-workbench .part.panel > .composite.title> .panel-switcher-container > .monaco-action-bar .action-item.left::before, -.monaco-workbench .part.panel > .composite.title> .panel-switcher-container > .monaco-action-bar .action-item.right::after { +.monaco-workbench .part.panel > .composite.title> .panel-switcher-container > .monaco-action-bar .action-item.right::after, +.monaco-workbench .part.panel > .composite.title.dragged-over > .panel-switcher-container > .monaco-action-bar .action-item:last-of-type::after { opacity: 1; } diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenActions.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenActions.ts index 88ab6dad4c..da553df5a5 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenActions.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenActions.ts @@ -39,6 +39,17 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.action.alternativeAcceptSelectedQuickOpenItem', + weight: KeybindingWeight.WorkbenchContrib, + when: inQuickOpenContext, + primary: 0, + handler: accessor => { + const quickInputService = accessor.get(IQuickInputService); + return quickInputService.accept({ ctrlCmd: true, alt: false }); + } +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.focusQuickOpen', weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index a27214e80c..9ea8145d9c 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -724,7 +724,7 @@ class EditorHistoryHandler { // Sort by score and provide a fallback sorter that keeps the // recency of items in case the score for items is the same - .sort((e1, e2) => compareItemsByScore(e1, e2, query, false, accessor, this.scorerCache, () => -1)); + .sort((e1, e2) => compareItemsByScore(e1, e2, query, false, accessor, this.scorerCache)); } } diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 5c03e9439a..b24676f0ac 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -21,8 +21,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Event, Emitter } from 'vs/base/common/event'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService, IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { SIDE_BAR_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND, SIDE_BAR_BORDER, SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EventType, addDisposableListener, trackFocus } from 'vs/base/browser/dom'; @@ -33,7 +33,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { LayoutPriority } from 'vs/base/browser/ui/grid/grid'; import { assertIsDefined } from 'vs/base/common/types'; -import { LocalSelectionTransfer, DraggedViewIdentifier, DraggedCompositeIdentifier } from 'vs/workbench/browser/dnd'; +import { CompositeDragAndDropObserver } from 'vs/workbench/browser/dnd'; export class SidebarPart extends CompositePart implements IViewletService { @@ -165,28 +165,18 @@ export class SidebarPart extends CompositePart implements IViewletServi })); this.titleLabelElement!.draggable = true; - this._register(addDisposableListener(this.titleLabelElement!, EventType.DRAG_START, e => { - const activeViewlet = this.getActiveViewlet(); - if (activeViewlet) { - const visibleViews = activeViewlet.getViewPaneContainer().views.filter(v => v.isVisible()); - if (visibleViews.length === 1) { - LocalSelectionTransfer.getInstance().setData([new DraggedViewIdentifier(visibleViews[0].id)], DraggedViewIdentifier.prototype); - } else { - LocalSelectionTransfer.getInstance().setData([new DraggedCompositeIdentifier(activeViewlet.getId())], DraggedCompositeIdentifier.prototype); - } - } - })); - this._register(addDisposableListener(this.titleLabelElement!, EventType.DRAG_END, e => { - if (LocalSelectionTransfer.getInstance().hasData(DraggedViewIdentifier.prototype)) { - LocalSelectionTransfer.getInstance().clearData(DraggedViewIdentifier.prototype); + const draggedItemProvider = (): { type: 'view' | 'composite', id: string } => { + const activeViewlet = this.getActiveViewlet()!; + const visibleViews = activeViewlet.getViewPaneContainer().views.filter(v => v.isVisible()); + if (visibleViews.length === 1) { + return { type: 'view', id: visibleViews[0].id }; + } else { + return { type: 'composite', id: activeViewlet.getId() }; } + }; - if (LocalSelectionTransfer.getInstance().hasData(DraggedCompositeIdentifier.prototype)) { - LocalSelectionTransfer.getInstance().clearData(DraggedCompositeIdentifier.prototype); - } - })); - + this._register(CompositeDragAndDropObserver.INSTANCE.registerDraggable(this.titleLabelElement!, draggedItemProvider, {})); return titleArea; } @@ -343,6 +333,23 @@ class FocusSideBarAction extends Action { } } +registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { + + // Sidebar Background: since views can host editors, we apply a background rule if the sidebar background + // color is different from the editor background color. This is a bit of a hack though. The better way + // would be to have a way to push the background color onto each editor widget itself somehow. + const sidebarBackground = theme.getColor(SIDE_BAR_BACKGROUND); + if (sidebarBackground && sidebarBackground !== theme.getColor(editorBackground)) { + collector.addRule(` + .monaco-workbench .part.sidebar > .content .monaco-editor, + .monaco-workbench .part.sidebar > .content .monaco-editor .margin, + .monaco-workbench .part.sidebar > .content .monaco-editor .monaco-editor-background { + background-color: ${sidebarBackground}; + } + `); + } +}); + const registry = Registry.as(ActionExtensions.WorkbenchActions); registry.registerWorkbenchAction(SyncActionDescriptor.create(FocusSideBarAction, FocusSideBarAction.ID, FocusSideBarAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_0 diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index ca64933c77..c5f9cbf4e3 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -609,7 +609,6 @@ export class TreeView extends Disposable implements ITreeView { return tree.expand(element, false); })); } - return Promise.resolve(undefined); } setSelection(items: ITreeItem[]): void { @@ -625,11 +624,10 @@ export class TreeView extends Disposable implements ITreeView { } } - reveal(item: ITreeItem): Promise { + async reveal(item: ITreeItem): Promise { if (this.tree) { - return Promise.resolve(this.tree.reveal(item)); + return this.tree.reveal(item); } - return Promise.resolve(); } private refreshing: boolean = false; @@ -689,11 +687,11 @@ class TreeDataSource implements IAsyncDataSource { return !!this.treeView.dataProvider && (element.collapsibleState !== TreeItemCollapsibleState.None); } - getChildren(element: ITreeItem): ITreeItem[] | Promise { + async getChildren(element: ITreeItem): Promise { if (this.treeView.dataProvider) { return this.withProgress(this.treeView.dataProvider.getChildren(element)); } - return Promise.resolve([]); + return []; } } diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 162f94bc58..8900958f2d 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -23,7 +23,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { PaneView, IPaneViewOptions, IPaneOptions, Pane } from 'vs/base/browser/ui/splitview/paneview'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewContainersRegistry, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IViewsRegistry, IViewContentDescriptor } from 'vs/workbench/common/views'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -209,8 +209,6 @@ export abstract class ViewPane extends Pane implements IView { this.title = options.title; this.showActionsAlways = !!options.showActionsAlways; this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); - this._preventCollapse = this.viewDescriptorService.getViewLocation(this.id) === ViewContainerLocation.Panel; - this._expanded = this._preventCollapse || this._expanded; this.menuActions = this._register(instantiationService.createInstance(ViewMenuActions, this.id, options.titleMenuId || MenuId.ViewTitle, MenuId.ViewTitleContext)); this._register(this.menuActions.onDidChangeTitle(() => this.updateActions())); @@ -275,9 +273,7 @@ export abstract class ViewPane extends Pane implements IView { protected renderHeader(container: HTMLElement): void { this.headerContainer = container; - if (!this._preventCollapse) { - this.renderTwisties(container); - } + this.renderTwisties(container); this.renderHeaderTitle(container, this.title); @@ -788,7 +784,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { create(parent: HTMLElement): void { const options = this.options as IPaneViewOptions; - options.orientation = this.viewDescriptorService.getViewContainerLocation(this.viewContainer) === ViewContainerLocation.Panel ? Orientation.HORIZONTAL : Orientation.VERTICAL; + options.orientation = this.orientation; this.paneview = this._register(new PaneView(parent, this.options)); this._register(this.paneview.onDidDrop(({ from, to }) => this.movePane(from as ViewPane, to as ViewPane))); this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e)))); @@ -919,8 +915,20 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } + private get orientation(): Orientation { + if (this.viewDescriptorService.getViewContainerLocation(this.viewContainer) === ViewContainerLocation.Sidebar) { + return Orientation.VERTICAL; + } else { + return this.layoutService.getPanelPosition() === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL; + } + } + layout(dimension: Dimension): void { if (this.paneview) { + if (this.paneview.orientation !== this.orientation) { + this.paneview.flipOrientation(dimension.height, dimension.width); + } + this.paneview.layout(dimension.height, dimension.width); } @@ -1142,7 +1150,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { let overlay: ViewPaneDropOverlay | undefined; - this._register(CompositeDragAndDropObserver.INSTANCE.registerDraggable(pane.draggableElement, 'view', pane.id, {})); + this._register(CompositeDragAndDropObserver.INSTANCE.registerDraggable(pane.draggableElement, () => { return { type: 'view', id: pane.id }; }, {})); this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(pane.dropTargetElement, { onDragEnter: (e) => { diff --git a/src/vs/workbench/browser/quickopen.ts b/src/vs/workbench/browser/quickopen.ts index 0ffb626688..2d604c76ae 100644 --- a/src/vs/workbench/browser/quickopen.ts +++ b/src/vs/workbench/browser/quickopen.ts @@ -32,6 +32,7 @@ export interface IWorkbenchQuickOpenConfiguration { }, quickOpen: { enableExperimentalNewVersion: boolean; + preserveInput: boolean; } }; } @@ -325,19 +326,14 @@ export class QuickOpenAction extends Action { id: string, label: string, prefix: string, - @IQuickOpenService private readonly quickOpenService: IQuickOpenService + @IQuickOpenService protected readonly quickOpenService: IQuickOpenService ) { super(id, label); this.prefix = prefix; - this.enabled = !!this.quickOpenService; } - run(): Promise { - - // Show with prefix + async run(): Promise { this.quickOpenService.show(this.prefix); - - return Promise.resolve(undefined); } } diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index e474ad7d71..462e8bfbfa 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -194,10 +194,6 @@ export class ShowViewletAction extends Action { export class CollapseAction extends Action { constructor(tree: AsyncDataTree | AbstractTree, enabled: boolean, clazz?: string) { - super('workbench.action.collapse', nls.localize('collapse', "Collapse All"), clazz, enabled, () => { - tree.collapseAll(); - - return Promise.resolve(undefined); - }); + super('workbench.action.collapse', nls.localize('collapse', "Collapse All"), clazz, enabled, async () => tree.collapseAll()); } } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index ea7b5d9b34..0da4b0aa11 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -182,7 +182,7 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio 'workbench.quickOpen.enableExperimentalNewVersion': { 'type': 'boolean', 'description': nls.localize('workbench.quickOpen.enableExperimentalNewVersion', "Will use the new quick open implementation for testing purposes."), - 'default': false + 'default': true }, 'workbench.settings.openDefaultSettings': { 'type': 'boolean', diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 919318aea4..83b2d5e8ca 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -168,6 +168,10 @@ export interface IFileEditorInputFactory { isFileEditorInput(obj: unknown): obj is IFileEditorInput; } +interface ICustomEditorInputFactory { + createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise; +} + export interface IEditorInputFactoryRegistry { /** @@ -180,6 +184,16 @@ export interface IEditorInputFactoryRegistry { */ getFileEditorInputFactory(): IFileEditorInputFactory; + /** + * Registers the custom editor input factory to use for custom inputs. + */ + registerCustomEditorInputFactory(factory: ICustomEditorInputFactory): void; + + /** + * Returns the custom editor input factory to use for custom inputs. + */ + getCustomEditorInputFactory(): ICustomEditorInputFactory; + /** * Registers a editor input factory for the given editor input to the registry. An editor input factory * is capable of serializing and deserializing editor inputs from string data. @@ -1387,6 +1401,7 @@ export interface IEditorMemento { class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { private instantiationService: IInstantiationService | undefined; private fileEditorInputFactory: IFileEditorInputFactory | undefined; + private customEditorInputFactory: ICustomEditorInputFactory | undefined; private readonly editorInputFactoryConstructors: Map> = new Map(); private readonly editorInputFactoryInstances: Map = new Map(); @@ -1414,6 +1429,14 @@ class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { return assertIsDefined(this.fileEditorInputFactory); } + registerCustomEditorInputFactory(factory: ICustomEditorInputFactory): void { + this.customEditorInputFactory = factory; + } + + getCustomEditorInputFactory(): ICustomEditorInputFactory { + return assertIsDefined(this.customEditorInputFactory); + } + registerEditorInputFactory(editorInputId: string, ctor: IConstructorSignature0): IDisposable { if (!this.instantiationService) { this.editorInputFactoryConstructors.set(editorInputId, ctor); diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index d40d4d0174..1cc9b58d9e 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -697,15 +697,13 @@ export class ChoiceAction extends Action { private readonly _keepOpen: boolean; constructor(id: string, choice: IPromptChoice) { - super(id, choice.label, undefined, true, () => { + super(id, choice.label, undefined, true, async () => { // Pass to runner choice.run(); // Emit Event this._onDidRun.fire(); - - return Promise.resolve(); }); this._keepOpen = !!choice.keepOpen; diff --git a/src/vs/workbench/contrib/backup/common/backupRestorer.ts b/src/vs/workbench/contrib/backup/common/backupRestorer.ts index 46dca1f7a5..d88567c882 100644 --- a/src/vs/workbench/contrib/backup/common/backupRestorer.ts +++ b/src/vs/workbench/contrib/backup/common/backupRestorer.ts @@ -10,9 +10,11 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { Schemas } from 'vs/base/common/network'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IUntitledTextResourceEditorInput, IEditorInput } from 'vs/workbench/common/editor'; +import { IUntitledTextResourceEditorInput, IEditorInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IEditorInputWithOptions } from 'vs/workbench/common/editor'; import { toLocalResource, isEqual } from 'vs/base/common/resources'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class BackupRestorer implements IWorkbenchContribution { @@ -24,7 +26,8 @@ export class BackupRestorer implements IWorkbenchContribution { @IEditorService private readonly editorService: IEditorService, @IBackupFileService private readonly backupFileService: IBackupFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { this.restoreBackups(); } @@ -80,13 +83,13 @@ export class BackupRestorer implements IWorkbenchContribution { private async doOpenEditors(resources: URI[]): Promise { const hasOpenedEditors = this.editorService.visibleEditors.length > 0; - const inputs = resources.map((resource, index) => this.resolveInput(resource, index, hasOpenedEditors)); + const inputs = await Promise.all(resources.map((resource, index) => this.resolveInput(resource, index, hasOpenedEditors))); // Open all remaining backups as editors and resolve them to load their backups await this.editorService.openEditors(inputs); } - private resolveInput(resource: URI, index: number, hasOpenedEditors: boolean): IResourceEditorInput | IUntitledTextResourceEditorInput { + private async resolveInput(resource: URI, index: number, hasOpenedEditors: boolean): Promise { const options = { pinned: true, preserveFocus: true, inactive: index > 0 || hasOpenedEditors }; // this is a (weak) strategy to find out if the untitled input had @@ -96,6 +99,12 @@ export class BackupRestorer implements IWorkbenchContribution { return { resource: toLocalResource(resource, this.environmentService.configuration.remoteAuthority), options, forceUntitled: true }; } + if (resource.scheme === Schemas.vscodeCustomEditor) { + const editor = await Registry.as(EditorExtensions.EditorInputFactories).getCustomEditorInputFactory() + .createCustomEditorInput(resource, this.instantiationService); + return { editor, options }; + } + return { resource, options }; } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts index dd6f7b4ddb..8a5453ba6a 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts @@ -399,7 +399,7 @@ export class BulkEditPreviewProvider implements ITextModelContentProvider { } // apply new edits and keep (future) undo edits const newEdits = this._operations.getFileEdits(uri); - const newUndoEdits = model.applyEdits(newEdits); + const newUndoEdits = model.applyEdits(newEdits, true); this._modelPreviewEdits.set(model.id, newUndoEdits); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index 802c00988c..fdcf917961 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -37,13 +37,14 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv return this.editorService.activeTextEditorControl; } - 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 { // Check for sideBySide use if ((options.keyMods.ctrlCmd || options.forceSideBySide) && this.editorService.activeEditor) { this.editorService.openEditor(this.editorService.activeEditor, { selection: options.range, - pinned: options.keyMods.alt || this.configuration.openEditorPinned + pinned: options.keyMods.alt || this.configuration.openEditorPinned, + preserveFocus: options.preserveFocus }, SIDE_GROUP); } @@ -58,5 +59,5 @@ Registry.as(Extensions.Quickaccess).registerQuickAccessPro ctor: GotoLineQuickAccessProvider, prefix: AbstractGotoLineQuickAccessProvider.PREFIX, placeholder: localize('gotoLineQuickAccessPlaceholder', "Type the line number and optional column to go to (e.g. 42:5 for line 42 and column 5)."), - helpEntries: [{ description: localize('gotoLineQuickAccess', "Go to Line"), needsEditor: true }] + helpEntries: [{ description: localize('gotoLineQuickAccess', "Go to Line/Column"), needsEditor: true }] }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index f1912bb2d4..1b7d510529 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -4,15 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { IKeyMods } from 'vs/platform/quickinput/common/quickInput'; +import { IKeyMods, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IEditor } from 'vs/editor/common/editorCommon'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IRange } from 'vs/editor/common/core/range'; import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; -import { AbstractGotoSymbolQuickAccessProvider } from 'vs/editor/contrib/quickAccess/gotoSymbolQuickAccess'; +import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from 'vs/editor/contrib/quickAccess/gotoSymbolQuickAccess'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; +import { ITextModel } from 'vs/editor/common/model'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { timeout } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider { @@ -40,13 +44,14 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess return this.editorService.activeTextEditorControl; } - 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 { // Check for sideBySide use if ((options.keyMods.ctrlCmd || options.forceSideBySide) && this.editorService.activeEditor) { this.editorService.openEditor(this.editorService.activeEditor, { selection: options.range, - pinned: options.keyMods.alt || this.configuration.openEditorPinned + pinned: options.keyMods.alt || this.configuration.openEditorPinned, + preserveFocus: options.preserveFocus }, SIDE_GROUP); } @@ -55,11 +60,45 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess super.gotoLocation(editor, options); } } + + + //#region public methods to use this picker from other pickers + + private static readonly SYMBOL_PICKS_TIMEOUT = 8000; + + async getSymbolPicks(model: ITextModel, filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { + + // If the registry does not know the model, we wait for as long as + // the registry knows it. This helps in cases where a language + // registry was not activated yet for providing any symbols. + // To not wait forever, we eventually timeout though. + const result = await Promise.race([ + this.waitForLanguageSymbolRegistry(model, disposables), + timeout(GotoSymbolQuickAccessProvider.SYMBOL_PICKS_TIMEOUT) + ]); + + if (!result || token.isCancellationRequested) { + return []; + } + + return this.doGetSymbolPicks(this.getDocumentSymbols(model, true, token), filter, token); + } + + addDecorations(editor: IEditor, range: IRange): void { + super.addDecorations(editor, range); + } + + clearDecorations(editor: IEditor): void { + super.clearDecorations(editor); + } + + //#endregion } Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ ctor: GotoSymbolQuickAccessProvider, prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, + contextKey: 'inFileSymbolsPicker', placeholder: localize('gotoSymbolQuickAccessPlaceholder', "Type the name of a symbol to go to."), helpEntries: [ { description: localize('gotoSymbolQuickAccess', "Go to Symbol in Editor"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, needsEditor: true }, diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts index 5408baeed9..9029c98715 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts @@ -19,7 +19,6 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { DefaultSettingsEditorContribution } from 'vs/workbench/contrib/preferences/browser/preferencesEditor'; -import { registerAndGetAmdImageURL } from 'vs/base/common/amd'; const transientWordWrapState = 'transientWordWrapState'; const isWordWrapMinifiedKey = 'isWordWrapMinified'; @@ -272,16 +271,12 @@ registerEditorContribution(ToggleWordWrapController.ID, ToggleWordWrapController registerEditorAction(ToggleWordWrapAction); -const WORD_WRAP_DARK_ICON = URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/codeEditor/browser/word-wrap-dark.svg')); -const WORD_WRAP_LIGHT_ICON = URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/codeEditor/browser/word-wrap-light.svg')); - MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_WORD_WRAP_ID, title: nls.localize('unwrapMinified', "Disable wrapping for this file"), icon: { - dark: WORD_WRAP_DARK_ICON, - light: WORD_WRAP_LIGHT_ICON + id: 'codicon/word-wrap' } }, group: 'navigation', @@ -297,8 +292,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { id: TOGGLE_WORD_WRAP_ID, title: nls.localize('wrapMinified', "Enable wrapping for this file"), icon: { - dark: WORD_WRAP_DARK_ICON, - light: WORD_WRAP_LIGHT_ICON + id: 'codicon/word-wrap' } }, group: 'navigation', diff --git a/src/vs/workbench/contrib/codeEditor/browser/word-wrap-dark.svg b/src/vs/workbench/contrib/codeEditor/browser/word-wrap-dark.svg deleted file mode 100644 index f54d621127..0000000000 --- a/src/vs/workbench/contrib/codeEditor/browser/word-wrap-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/codeEditor/browser/word-wrap-light.svg b/src/vs/workbench/contrib/codeEditor/browser/word-wrap-light.svg deleted file mode 100644 index 6a8e3fdf93..0000000000 --- a/src/vs/workbench/contrib/codeEditor/browser/word-wrap-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index dbcea2fe9a..24e85ddd37 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -3,21 +3,41 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; -import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory'; -import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { Lazy } from 'vs/base/common/lazy'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorInput } from 'vs/workbench/common/editor'; +import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; +import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory'; +import { IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; + +export interface CustomDocumentBackupData { + readonly viewType: string; + readonly editorResource: UriComponents; + readonly extension: undefined | { + readonly location: UriComponents; + readonly id: string; + }; + + readonly webview: { + readonly id: string; + readonly options: WebviewInputOptions; + readonly state: any; + }; +} export class CustomEditorInputFactory extends WebviewEditorInputFactory { public static readonly ID = CustomEditorInput.typeId; public constructor( + @IWebviewWorkbenchService webviewWorkbenchService: IWebviewWorkbenchService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IWebviewWorkbenchService private readonly webviewWorkbenchService: IWebviewWorkbenchService, + @IWebviewService private readonly _webviewService: IWebviewService, ) { super(webviewWorkbenchService); } @@ -43,11 +63,19 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { const id = data.id || generateUuid(); const webview = new Lazy(() => { - const webviewInput = this.webviewWorkbenchService.reviveWebview(id, data.viewType, data.title, data.iconPath, data.state, data.options, data.extensionLocation && data.extensionId ? { - location: data.extensionLocation, - id: data.extensionId - } : undefined, data.group); - return webviewInput.webview; + const webview = this._webviewService.createWebviewOverlay(id, { + enableFindWidget: data.options.enableFindWidget, + retainContextWhenHidden: data.options.retainContextWhenHidden + }, data.options); + + if (data.extensionLocation && data.extensionId) { + webview.extension = { + location: data.extensionLocation, + id: data.extensionId + }; + } + + return webview; }); const customInput = this._instantiationService.createInstance(CustomEditorInput, URI.from((data as any).editorResource), data.viewType, id, webview); @@ -56,4 +84,37 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { } return customInput; } + + public static createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise { + return instantiationService.invokeFunction(async accessor => { + const webviewService = accessor.get(IWebviewService); + const backupFileService = accessor.get(IBackupFileService); + + const backup = await backupFileService.resolve(resource); + if (!backup) { + throw new Error(`No backup found for custom editor: ${resource}`); + } + + const backupData = backup.meta as CustomDocumentBackupData; + const id = backupData.webview.id; + + const webview = new Lazy(() => { + const webview = webviewService.createWebviewOverlay(id, { + enableFindWidget: backupData.webview.options.enableFindWidget, + retainContextWhenHidden: backupData.webview.options.retainContextWhenHidden + }, backupData.webview.options); + + webview.extension = backupData.extension ? { + location: URI.revive(backupData.extension.location), + id: new ExtensionIdentifier(backupData.extension.id), + } : undefined; + + return webview; + }); + + const editor = instantiationService.createInstance(CustomEditorInput, URI.revive(backupData.editorResource), backupData.viewType, id, webview); + editor.updateGroup(0); + return editor; + }); + } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 470f36ed34..cc12a8865f 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -306,7 +306,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ private updateContexts() { const activeEditorPane = this.editorService.activeEditorPane; - const resource = activeEditorPane?.input.resource; + const resource = activeEditorPane?.input?.resource; if (!resource) { this._customEditorContextKey.reset(); this._focusedCustomEditorIsEditable.reset(); diff --git a/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts b/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts index 39449d386c..1e83d4eaf4 100644 --- a/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts +++ b/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts @@ -38,6 +38,8 @@ Registry.as(EditorInputExtensions.EditorInputFactor CustomEditorInputFactory.ID, CustomEditorInputFactory); +Registry.as(EditorInputExtensions.EditorInputFactories).registerCustomEditorInputFactory(CustomEditorInputFactory); + Registry.as(ConfigurationExtensions.Configuration) .registerConfiguration({ ...workbenchConfigurationNodeBase, diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index bf43984986..2c23b503f1 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -95,13 +95,15 @@ const VIEW_CONTAINER: ViewContainer = Registry.as(ViewE focusCommand: { id: OpenDebugPanelAction.ID, keybindings: openPanelKb - } + }, + hideIfEmpty: true }, ViewContainerLocation.Panel); Registry.as(ViewExtensions.ViewsRegistry).registerViews([{ id: REPL_VIEW_ID, name: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugPanel' }, 'Debug Console'), canToggleVisibility: false, + canMoveView: true, ctorDescriptor: new SyncDescriptor(Repl), }], VIEW_CONTAINER); @@ -180,6 +182,7 @@ registerDebugCommandPaletteItem(TOGGLE_INLINE_BREAKPOINT_ID, nls.localize('inlin Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ ctor: StartDebugQuickAccessProvider, prefix: StartDebugQuickAccessProvider.PREFIX, + contextKey: 'inLaunchConfigurationsPicker', placeholder: nls.localize('startDebugPlaceholder', "Type the name of a launch configuration to run."), helpEntries: [{ description: nls.localize('startDebugHelp', "Start Debug Configurations"), needsEditor: false }] }); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 8e9df08be7..c4bad13838 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -760,8 +760,9 @@ export class DebugService implements IDebugService { const control = editor.getControl(); if (stackFrame && isCodeEditor(control) && control.hasModel()) { const model = control.getModel(); - if (stackFrame.range.startLineNumber <= model.getLineCount()) { - const lineContent = control.getModel().getLineContent(stackFrame.range.startLineNumber); + const lineNumber = stackFrame.range.startLineNumber; + if (lineNumber >= 1 && lineNumber <= model.getLineCount()) { + const lineContent = control.getModel().getLineContent(lineNumber); aria.alert(nls.localize('debuggingPaused', "Debugging paused {0}, {1} {2} {3}", thread && thread.stoppedDetails ? `, reason ${thread.stoppedDetails.reason}` : '', stackFrame.source ? stackFrame.source.name : '', stackFrame.range.startLineNumber, lineContent)); } } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index f6773ac441..57191d3742 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -50,7 +50,6 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/textRe import { RunOnceScheduler } from 'vs/base/common/async'; import { FuzzyScore } from 'vs/base/common/filters'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { ReplDelegate, ReplVariablesRenderer, ReplSimpleElementsRenderer, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplRawObjectsRenderer, ReplDataSource, ReplAccessibilityProvider, ReplGroupRenderer } from 'vs/workbench/contrib/debug/browser/replViewer'; import { localize } from 'vs/nls'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; @@ -495,7 +494,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { setRowLineHeight: false, supportDynamicHeights: wordWrap, overrideStyles: { - listBackground: PANEL_BACKGROUND + listBackground: this.getBackgroundColor() } }); this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); diff --git a/src/vs/workbench/contrib/debug/common/debugUtils.ts b/src/vs/workbench/contrib/debug/common/debugUtils.ts index 8dfe002e01..6933c7ad5e 100644 --- a/src/vs/workbench/contrib/debug/common/debugUtils.ts +++ b/src/vs/workbench/contrib/debug/common/debugUtils.ts @@ -128,10 +128,12 @@ function stringToUri(source: PathContainer): string | undefined { function uriToString(source: PathContainer): string | undefined { if (typeof source.path === 'object') { const u = uri.revive(source.path); - if (u.scheme === 'file') { - return u.fsPath; - } else { - return u.toString(); + if (u) { + if (u.scheme === 'file') { + return u.fsPath; + } else { + return u.toString(); + } } } return source.path; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 58c950049b..13cab92fa0 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -672,7 +672,7 @@ export class ExtensionEditor extends BaseEditor { body { padding: 10px 20px; line-height: 22px; - max-width: 780px; + max-width: 882px; margin: 0 auto; } diff --git a/src/vs/workbench/contrib/extensions/browser/media/profile-start-dark.svg b/src/vs/workbench/contrib/extensions/browser/media/profile-start-dark.svg deleted file mode 100644 index a60d77cd37..0000000000 --- a/src/vs/workbench/contrib/extensions/browser/media/profile-start-dark.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/vs/workbench/contrib/extensions/browser/media/profile-start-light.svg b/src/vs/workbench/contrib/extensions/browser/media/profile-start-light.svg deleted file mode 100644 index f541ed4d51..0000000000 --- a/src/vs/workbench/contrib/extensions/browser/media/profile-start-light.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/vs/workbench/contrib/extensions/browser/media/profile-stop-dark.svg b/src/vs/workbench/contrib/extensions/browser/media/profile-stop-dark.svg deleted file mode 100644 index a0948780ee..0000000000 --- a/src/vs/workbench/contrib/extensions/browser/media/profile-stop-dark.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/vs/workbench/contrib/extensions/browser/media/profile-stop-light.svg b/src/vs/workbench/contrib/extensions/browser/media/profile-stop-light.svg deleted file mode 100644 index d9222c3c31..0000000000 --- a/src/vs/workbench/contrib/extensions/browser/media/profile-stop-light.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/vs/workbench/contrib/extensions/browser/media/save-dark.svg b/src/vs/workbench/contrib/extensions/browser/media/save-dark.svg deleted file mode 100644 index 8acad37a99..0000000000 --- a/src/vs/workbench/contrib/extensions/browser/media/save-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/extensions/browser/media/save-light.svg b/src/vs/workbench/contrib/extensions/browser/media/save-light.svg deleted file mode 100644 index 529e489a81..0000000000 --- a/src/vs/workbench/contrib/extensions/browser/media/save-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/extensions/browser/media/start-dark.svg b/src/vs/workbench/contrib/extensions/browser/media/start-dark.svg deleted file mode 100644 index 8b0a58eca9..0000000000 --- a/src/vs/workbench/contrib/extensions/browser/media/start-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/extensions/browser/media/start-light.svg b/src/vs/workbench/contrib/extensions/browser/media/start-light.svg deleted file mode 100644 index 2563bfa114..0000000000 --- a/src/vs/workbench/contrib/extensions/browser/media/start-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index 0b71fdf9a4..8af80587e0 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -18,10 +18,8 @@ import { RuntimeExtensionsEditor, ShowRuntimeExtensionsAction, IExtensionHostPro import { EditorInput, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, ActiveEditorContext } from 'vs/workbench/common/editor'; import { ExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-browser/extensionProfileService'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput'; -import { URI } from 'vs/base/common/uri'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionsAutoProfiler } from 'vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler'; -import { registerAndGetAmdImageURL } from 'vs/base/common/amd'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { OpenExtensionsFolderAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -106,8 +104,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { id: DebugExtensionHostAction.ID, title: DebugExtensionHostAction.LABEL, icon: { - dark: URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/extensions/browser/media/start-dark.svg`)), - light: URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/extensions/browser/media/start-light.svg`)), + id: 'codicon/debug-start' } }, group: 'navigation', @@ -119,8 +116,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { id: StartExtensionHostProfileAction.ID, title: StartExtensionHostProfileAction.LABEL, icon: { - dark: URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/extensions/browser/media/profile-start-dark.svg`)), - light: URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/extensions/browser/media/profile-start-light.svg`)), + id: 'codicon/circle-filled' } }, group: 'navigation', @@ -132,8 +128,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { id: StopExtensionHostProfileAction.ID, title: StopExtensionHostProfileAction.LABEL, icon: { - dark: URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/extensions/browser/media/profile-stop-dark.svg`)), - light: URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/extensions/browser/media/profile-stop-light.svg`)), + id: 'codicon/debug-stop' } }, group: 'navigation', @@ -145,8 +140,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { id: SaveExtensionHostProfileAction.ID, title: SaveExtensionHostProfileAction.LABEL, icon: { - dark: URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/extensions/browser/media/save-dark.svg`)), - light: URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/extensions/browser/media/save-light.svg`)), + id: 'codicon/save-all' }, precondition: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED }, diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index f53cbf4402..aefa8d1887 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -111,6 +111,7 @@ class ToggleMarkersPanelAction extends TogglePanelAction { const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: Constants.MARKERS_CONTAINER_ID, name: Messages.MARKERS_PANEL_TITLE_PROBLEMS, + hideIfEmpty: true, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [Constants.MARKERS_CONTAINER_ID, Constants.MARKERS_VIEW_STORAGE_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), focusCommand: { id: ToggleMarkersPanelAction.ID, keybindings: { @@ -123,6 +124,7 @@ Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews id: Constants.MARKERS_VIEW_ID, name: Messages.MARKERS_PANEL_TITLE_PROBLEMS, canToggleVisibility: false, + canMoveView: true, ctorDescriptor: new SyncDescriptor(MarkersView), }], VIEW_CONTAINER); diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index 2f2bfbfe19..1c49de445b 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -44,7 +44,6 @@ import { withUndefinedAsNull } from 'vs/base/common/types'; import { MementoObject, Memento } from 'vs/workbench/common/memento'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { KeyCode } from 'vs/base/common/keyCodes'; import { editorLightBulbForeground, editorLightBulbAutoFixForeground } from 'vs/platform/theme/common/colorRegistry'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; @@ -419,7 +418,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { dnd: new ResourceDragAndDrop(this.instantiationService), expandOnlyOnTwistieClick: (e: TreeElement) => e instanceof Marker && e.relatedInformation.length > 0, overrideStyles: { - listBackground: PANEL_BACKGROUND + listBackground: this.getBackgroundColor() } }, )); diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index d0bc9ccdc0..2e86f5233e 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -26,5 +26,10 @@ export const EDITOR_BOTTOM_PADDING = 8; export const EDITOR_TOOLBAR_HEIGHT = 22; export const RUN_BUTTON_WIDTH = 20; -// Context Keys -export const NOTEBOOK_CELL_TYPE_CONTEXT_KEY = 'notebookCellType'; +// Cell context keys +export const NOTEBOOK_CELL_TYPE_CONTEXT_KEY = 'notebookCellType'; // code, markdown +export const NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY = 'notebookCellEditable'; // bool +export const NOTEBOOK_CELL_MARKDOWN_EDIT_MODE_CONTEXT_KEY = 'notebookCellMarkdownEditMode'; // bool + +// Notebook context keys +export const NOTEBOOK_EDITABLE_CONTEXT_KEY = 'notebookEditable'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts index fe687a9a69..f940966bef 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts @@ -11,12 +11,21 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { InputFocusedContext, InputFocusedContextKey, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { COPY_CELL_DOWN_COMMAND_ID, COPY_CELL_UP_COMMAND_ID, DELETE_CELL_COMMAND_ID, EDIT_CELL_COMMAND_ID, EXECUTE_CELL_COMMAND_ID, INSERT_CODE_CELL_ABOVE_COMMAND_ID, INSERT_CODE_CELL_BELOW_COMMAND_ID, INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, MOVE_CELL_DOWN_COMMAND_ID, MOVE_CELL_UP_COMMAND_ID, SAVE_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellRenderTemplate, CellState, ICellViewModel, INotebookEditor, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { COPY_CELL_DOWN_COMMAND_ID, COPY_CELL_UP_COMMAND_ID, DELETE_CELL_COMMAND_ID, EDIT_CELL_COMMAND_ID, EXECUTE_CELL_COMMAND_ID, INSERT_CODE_CELL_ABOVE_COMMAND_ID, INSERT_CODE_CELL_BELOW_COMMAND_ID, INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, MOVE_CELL_DOWN_COMMAND_ID, MOVE_CELL_UP_COMMAND_ID, SAVE_CELL_COMMAND_ID, NOTEBOOK_CELL_TYPE_CONTEXT_KEY, NOTEBOOK_EDITABLE_CONTEXT_KEY, NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE_CONTEXT_KEY } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellRenderTemplate, CellEditState, ICellViewModel, INotebookEditor, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, CellRunState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +const enum CellToolbarOrder { + MoveCellUp, + MoveCellDown, + EditCell, + SaveCell, + InsertCell, + DeleteCell +} + registerAction2(class extends Action2 { constructor() { super({ @@ -29,7 +38,8 @@ registerAction2(class extends Action2 { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter }, weight: KeybindingWeight.WorkbenchContrib - } + }, + icon: { id: 'codicon/play' } }); } @@ -41,7 +51,7 @@ registerAction2(class extends Action2 { } } - runCell(accessor, context); + runCell(context); } }); @@ -159,6 +169,29 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.executeNotebookCell', + title: localize('notebookActions.executeNotebookCell', "Execute Notebook Active Cell") + }); + } + + async run(accessor: ServicesAccessor): Promise { + let editorService = accessor.get(IEditorService); + let editor = getActiveNotebookEditor(editorService); + + if (!editor) { + return; + } + + let activeCell = editor.getActiveCell(); + if (activeCell) { + return editor.executeNotebookCell(activeCell); + } + } +}); + registerAction2(class extends Action2 { constructor() { super({ @@ -183,7 +216,7 @@ registerAction2(class extends Action2 { let activeCell = editor.getActiveCell(); if (activeCell) { if (activeCell.cellKind === CellKind.Markdown) { - activeCell.state = CellState.Preview; + activeCell.editState = CellEditState.Preview; } editor.focusNotebookCell(activeCell, false); @@ -300,67 +333,26 @@ function getActiveNotebookEditor(editorService: IEditorService): INotebookEditor async function runActiveCell(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); - const notebookService = accessor.get(INotebookService); - - const resource = editorService.activeEditor?.resource; - if (!resource) { - return undefined; // {{SQL CARBON EDIT}} strict-null-check - } - const editor = getActiveNotebookEditor(editorService); if (!editor) { return undefined; // {{SQL CARBON EDIT}} strict-null-check } - const notebookProviders = notebookService.getContributedNotebookProviders(resource); - if (!notebookProviders.length) { - return undefined; // {{SQL CARBON EDIT}} strict-null-check - } - const activeCell = editor.getActiveCell(); if (!activeCell) { return undefined; // {{SQL CARBON EDIT}} strict-null-check } - const idx = editor.viewModel?.getViewCellIndex(activeCell); - if (typeof idx !== 'number') { - return undefined; // {{SQL CARBON EDIT}} strict-null-check - } - - const viewType = notebookProviders[0].id; - await notebookService.executeNotebookActiveCell(viewType, resource); - + editor.executeNotebookCell(activeCell); return activeCell; } -async function runCell(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - const progress = context.cellTemplate!.progressBar!; - progress.infinite().show(500); - - const editorService = accessor.get(IEditorService); - const notebookService = accessor.get(INotebookService); - - const resource = editorService.activeEditor?.resource; - if (!resource) { +async function runCell(context: INotebookCellActionContext): Promise { + if (context.cell.runState === CellRunState.Running) { return; } - const editor = getActiveNotebookEditor(editorService); - if (!editor) { - return; - } - - const notebookProviders = notebookService.getContributedNotebookProviders(resource); - if (!notebookProviders.length) { - return; - } - - // Need to make active, maybe TODO - editor.focusNotebookCell(context.cell, false); - - const viewType = notebookProviders[0].id; - await notebookService.executeNotebookActiveCell(viewType, resource); - progress.hide(); + return context.notebookEditor.executeNotebookCell(context.cell); } async function changeActiveCellToKind(kind: CellKind, accessor: ServicesAccessor): Promise { @@ -458,7 +450,18 @@ registerAction2(class extends InsertCellCommand { super( { id: INSERT_CODE_CELL_BELOW_COMMAND_ID, - title: localize('notebookActions.insertCodeCellBelow', "Insert Code Cell Below") + title: localize('notebookActions.insertCodeCellBelow', "Insert Code Cell Below"), + icon: { id: 'codicon/add' }, + menu: { + id: MenuId.NotebookCellTitle, + order: CellToolbarOrder.InsertCell, + alt: { + id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, + title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"), + icon: { id: 'codicon/add' }, + }, + when: ContextKeyExpr.equals(NOTEBOOK_EDITABLE_CONTEXT_KEY, true) + } }, CellKind.Code, 'below'); @@ -482,89 +485,13 @@ registerAction2(class extends InsertCellCommand { super( { id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, - title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"), + title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below") }, CellKind.Markdown, 'below'); } }); -export class InsertCodeCellAboveAction extends MenuItemAction { - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService commandService: ICommandService - ) { - super( - { - id: INSERT_CODE_CELL_ABOVE_COMMAND_ID, - title: localize('notebookActions.insertCodeCellAbove', "Insert Code Cell Above"), - icon: { id: 'codicon/add' } - }, - undefined, - { shouldForwardArgs: true }, - contextKeyService, - commandService); - } -} - -export class InsertCodeCellBelowAction extends MenuItemAction { - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService commandService: ICommandService - ) { - super( - { - id: INSERT_CODE_CELL_BELOW_COMMAND_ID, - title: localize('notebookActions.insertCodeCellBelow', "Insert Code Cell Below"), - icon: { id: 'codicon/add' } - }, - { - id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, - title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"), - icon: { id: 'codicon/add' } - }, - { shouldForwardArgs: true }, - contextKeyService, - commandService); - } -} - -export class InsertMarkdownCellAboveAction extends MenuItemAction { - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService commandService: ICommandService - ) { - super( - { - id: INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, - title: localize('notebookActions.insertMarkdownCellAbove', "Insert Markdown Cell Above"), - icon: { id: 'codicon/add' } - }, - undefined, - { shouldForwardArgs: true }, - contextKeyService, - commandService); - } -} - -export class InsertMarkdownCellBelowAction extends MenuItemAction { - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService commandService: ICommandService - ) { - super( - { - id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, - title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"), - icon: { id: 'codicon/add' } - }, - undefined, - { shouldForwardArgs: true }, - contextKeyService, - commandService); - } -} - registerAction2(class extends Action2 { constructor() { super( @@ -575,7 +502,16 @@ registerAction2(class extends Action2 { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), primary: KeyCode.Enter, weight: KeybindingWeight.WorkbenchContrib - } + }, + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and( + ContextKeyExpr.equals(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'markdown'), + ContextKeyExpr.equals(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE_CONTEXT_KEY, false), + ContextKeyExpr.equals(NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, true)), + order: CellToolbarOrder.EditCell + }, + icon: { id: 'codicon/pencil' } }); } @@ -591,30 +527,21 @@ registerAction2(class extends Action2 { } }); -export class EditCellAction extends MenuItemAction { - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService commandService: ICommandService - ) { - super( - { - id: EDIT_CELL_COMMAND_ID, - title: localize('notebookActions.editCell', "Edit Cell"), - icon: { id: 'codicon/pencil' } - }, - undefined, - { shouldForwardArgs: true }, - contextKeyService, - commandService); - } -} - registerAction2(class extends Action2 { constructor() { super( { id: SAVE_CELL_COMMAND_ID, - title: localize('notebookActions.saveCell', "Save Cell") + title: localize('notebookActions.saveCell', "Save Cell"), + menu: { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and( + ContextKeyExpr.equals(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'markdown'), + ContextKeyExpr.equals(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE_CONTEXT_KEY, true), + ContextKeyExpr.equals(NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, true)), + order: CellToolbarOrder.SaveCell + }, + icon: { id: 'codicon/save' } }); } @@ -630,30 +557,19 @@ registerAction2(class extends Action2 { } }); -export class SaveCellAction extends MenuItemAction { - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService commandService: ICommandService - ) { - super( - { - id: SAVE_CELL_COMMAND_ID, - title: localize('notebookActions.saveCell', "Save Cell"), - icon: { id: 'codicon/save' } - }, - undefined, - { shouldForwardArgs: true }, - contextKeyService, - commandService); - } -} registerAction2(class extends Action2 { constructor() { super( { id: DELETE_CELL_COMMAND_ID, - title: localize('notebookActions.deleteCell', "Delete Cell") + title: localize('notebookActions.deleteCell', "Delete Cell"), + menu: { + id: MenuId.NotebookCellTitle, + order: CellToolbarOrder.DeleteCell, + when: ContextKeyExpr.equals(NOTEBOOK_EDITABLE_CONTEXT_KEY, true) + }, + icon: { id: 'codicon/x' } }); } @@ -669,26 +585,6 @@ registerAction2(class extends Action2 { } }); -export class DeleteCellAction extends MenuItemAction { - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService commandService: ICommandService - ) { - super( - { - id: DELETE_CELL_COMMAND_ID, - title: localize('notebookActions.deleteCell', "Delete Cell"), - icon: { id: 'codicon/x' } - }, - undefined, - { shouldForwardArgs: true }, - contextKeyService, - commandService); - - this.class = 'codicon-x'; - } -} - async function moveCell(context: INotebookCellActionContext, direction: 'up' | 'down'): Promise { direction === 'up' ? context.notebookEditor.moveCellUp(context.cell) : @@ -706,7 +602,18 @@ registerAction2(class extends Action2 { super( { id: MOVE_CELL_UP_COMMAND_ID, - title: localize('notebookActions.moveCellUp', "Move Cell Up") + title: localize('notebookActions.moveCellUp', "Move Cell Up"), + icon: { id: 'codicon/arrow-up' }, + menu: { + id: MenuId.NotebookCellTitle, + order: CellToolbarOrder.MoveCellUp, + alt: { + id: COPY_CELL_UP_COMMAND_ID, + title: localize('notebookActions.copyCellUp', "Copy Cell Up"), + icon: { id: 'codicon/arrow-up' } + }, + when: ContextKeyExpr.equals(NOTEBOOK_EDITABLE_CONTEXT_KEY, true) + }, }); } @@ -722,34 +629,23 @@ registerAction2(class extends Action2 { } }); -export class MoveCellUpAction extends MenuItemAction { - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService commandService: ICommandService - ) { - super( - { - id: MOVE_CELL_UP_COMMAND_ID, - title: localize('notebookActions.moveCellUp', "Move Cell Up"), - icon: { id: 'codicon/arrow-up' } - }, - { - id: COPY_CELL_UP_COMMAND_ID, - title: localize('notebookActions.copyCellUp', "Copy Cell Up"), - icon: { id: 'codicon/arrow-up' } - }, - { shouldForwardArgs: true }, - contextKeyService, - commandService); - } -} - registerAction2(class extends Action2 { constructor() { super( { id: MOVE_CELL_DOWN_COMMAND_ID, - title: localize('notebookActions.moveCellDown', "Move Cell Down") + title: localize('notebookActions.moveCellDown', "Move Cell Down"), + icon: { id: 'codicon/arrow-down' }, + menu: { + id: MenuId.NotebookCellTitle, + order: CellToolbarOrder.MoveCellDown, + alt: { + id: COPY_CELL_DOWN_COMMAND_ID, + title: localize('notebookActions.copyCellDown', "Copy Cell Down"), + icon: { id: 'codicon/arrow-down' } + }, + when: ContextKeyExpr.equals(NOTEBOOK_EDITABLE_CONTEXT_KEY, true) + }, }); } @@ -765,30 +661,6 @@ registerAction2(class extends Action2 { } }); -export class MoveCellDownAction extends MenuItemAction { - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService commandService: ICommandService - ) { - super( - { - id: MOVE_CELL_DOWN_COMMAND_ID, - title: localize('notebookActions.moveCellDown', "Move Cell Down"), - icon: { id: 'codicon/arrow-down' } - }, - { - id: COPY_CELL_DOWN_COMMAND_ID, - title: localize('notebookActions.copyCellDown', "Copy Cell Down"), - icon: { id: 'codicon/arrow-down' } - }, - { shouldForwardArgs: true }, - contextKeyService, - commandService); - - this.class = 'codicon-arrow-down'; - } -} - registerAction2(class extends Action2 { constructor() { super( diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget.ts index 8c893e9a8d..49735d2d57 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget.ts @@ -5,7 +5,7 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellFindMatch, CellState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellFindMatch, CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { FindDecorations } from 'vs/editor/contrib/find/findDecorations'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; @@ -118,7 +118,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget { } private revealCellRange(cellIndex: number, matchIndex: number) { - this._findMatches[cellIndex].cell.state = CellState.Editing; + this._findMatches[cellIndex].cell.editState = CellEditState.Editing; this._notebookEditor.selectElement(this._findMatches[cellIndex].cell); this._notebookEditor.setCellSelection(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); this._notebookEditor.revealRangeInCenterIfOutsideViewport(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 58e8f107c4..6c6f44f3c2 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -4,23 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; -import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; -import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; -import { IOutput, CellKind, IRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { NotebookViewModel, IModelDecorationsChangeAccessor } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { FindMatch } from 'vs/editor/common/model'; -import { Range } from 'vs/editor/common/core/range'; +import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { Range } from 'vs/editor/common/core/range'; +import { FindMatch } from 'vs/editor/common/model'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { IModelDecorationsChangeAccessor, NotebookViewModel, CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellKind, IOutput, IRenderOutput, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NOTEBOOK_EDITABLE_CONTEXT_KEY } from 'vs/workbench/contrib/notebook/browser/constants'; export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); export const NOTEBOOK_EDITOR_FOCUSED = new RawContextKey('notebookEditorFocused', false); +export const NOTEBOOK_EDITOR_EDITABLE = new RawContextKey(NOTEBOOK_EDITABLE_CONTEXT_KEY, true); export interface NotebookLayoutInfo { width: number; @@ -28,14 +29,49 @@ export interface NotebookLayoutInfo { fontInfo: BareFontInfo; } +export interface NotebookLayoutChangeEvent { + width?: boolean; + height?: boolean; + fontInfo?: boolean; +} + +export interface CodeCellLayoutInfo { + readonly fontInfo: BareFontInfo | null; + readonly editorHeight: number; + readonly editorWidth: number; + readonly totalHeight: number; + readonly outputTotalHeight: number; + readonly indicatorHeight: number; +} + +export interface CodeCellLayoutChangeEvent { + editorHeight?: boolean; + outputHeight?: boolean; + totalHeight?: boolean; + outerWidth?: number; + font?: BareFontInfo; +} + +export interface MarkdownCellLayoutInfo { + readonly fontInfo: BareFontInfo | null; + readonly editorWidth: number; +} + +export interface MarkdownCellLayoutChangeEvent { + font?: BareFontInfo; + outerWidth?: number; +} + export interface ICellViewModel { readonly id: string; handle: number; uri: URI; cellKind: CellKind; - state: CellState; + editState: CellEditState; + runState: CellRunState; focusMode: CellFocusMode; getText(): string; + metadata: NotebookCellMetadata | undefined; } export interface INotebookEditor { @@ -104,6 +140,11 @@ export interface INotebookEditor { */ focusNotebookCell(cell: ICellViewModel, focusEditor: boolean): void; + /** + * Execute the given notebook cell + */ + executeNotebookCell(cell: ICellViewModel): Promise; + /** * Get current active cell */ @@ -207,11 +248,13 @@ export interface CellRenderTemplate { toolbar: ToolBar; focusIndicator?: HTMLElement; runToolbar?: ToolBar; + runButtonContainer?: HTMLElement; editingContainer?: HTMLElement; outputContainer?: HTMLElement; editor?: CodeEditorWidget; progressBar?: ProgressBar; disposables: DisposableStore; + toJSON(): void; } export interface IOutputTransformContribution { @@ -238,7 +281,12 @@ export enum CellRevealPosition { Center } -export enum CellState { +export enum CellRunState { + Idle, + Running +} + +export enum CellEditState { /** * Default state. * For markdown cell, it's Markdown preview. diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 8563e504fe..a865aebce6 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -22,13 +22,13 @@ import { contrastBorder, editorBackground, focusBorder, foreground, textBlockQuo import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorOptions, IEditorMemento, IEditorCloseEvent } from 'vs/workbench/common/editor'; -import { INotebookEditor, NotebookLayoutInfo, CellState, NOTEBOOK_EDITOR_FOCUSED, CellFocusMode, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, NotebookLayoutInfo, CellEditState, NOTEBOOK_EDITOR_FOCUSED, CellFocusMode, ICellViewModel, CellRunState, NOTEBOOK_EDITOR_EDITABLE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorInput, NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; import { CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; -import { IOutput, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IOutput, CellKind, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -38,12 +38,13 @@ import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { Emitter, Event } from 'vs/base/common/event'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { NotebookFindWidget } from 'vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget'; -import { NotebookViewModel, INotebookEditorViewState, IModelDecorationsChangeAccessor } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { NotebookViewModel, INotebookEditorViewState, IModelDecorationsChangeAccessor, CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { Range } from 'vs/editor/common/core/range'; import { CELL_MARGIN, RUN_BUTTON_WIDTH } from 'vs/workbench/contrib/notebook/browser/constants'; import { Color, RGBA } from 'vs/base/common/color'; +import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; const $ = DOM.$; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -83,9 +84,7 @@ export class NotebookCodeEditors implements ICompositeCodeEditor { get activeCodeEditor(): IEditor | undefined { const [focused] = this._list.getFocusedElements(); - return focused instanceof CellViewModel - ? this._renderedEditors.get(focused) - : undefined; + return this._renderedEditors.get(focused); } } @@ -97,6 +96,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { private list: NotebookCellList | undefined; private control: ICompositeCodeEditor | undefined; private renderedEditors: Map = new Map(); + private eventDispatcher: NotebookEventDispatcher | undefined; private notebookViewModel: NotebookViewModel | undefined; private localStore: DisposableStore = this._register(new DisposableStore()); private editorMemento: IEditorMemento; @@ -104,6 +104,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { private fontInfo: BareFontInfo | undefined; private dimension: DOM.Dimension | null = null; private editorFocus: IContextKey | null = null; + private editorEditable: IContextKey | null = null; private outputRenderer: OutputRenderer; private findWidget: NotebookFindWidget; @@ -158,6 +159,9 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this._register(this.onDidBlur(() => { this.editorFocus?.set(false); })); + + this.editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.contextKeyService); + this.editorEditable.set(true); } private generateFontInfo(): void { @@ -177,8 +181,8 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { DOM.addClass(this.body, 'cell-list-container'); const renders = [ - this.instantiationService.createInstance(CodeCellRenderer, this, this.renderedEditors), - this.instantiationService.createInstance(MarkdownCellRenderer, this), + this.instantiationService.createInstance(CodeCellRenderer, this, this.contextKeyService, this.renderedEditors), + this.instantiationService.createInstance(MarkdownCellRenderer, this.contextKeyService, this), ]; this.list = this.instantiationService.createInstance( @@ -333,10 +337,17 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.list?.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element); } - this.notebookViewModel = this.instantiationService.createInstance(NotebookViewModel, input.viewType!, model); + this.eventDispatcher = new NotebookEventDispatcher(); + this.notebookViewModel = this.instantiationService.createInstance(NotebookViewModel, input.viewType!, model, this.eventDispatcher); + this.editorEditable?.set(!!this.notebookViewModel.metadata?.editable); + this.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); const viewState = this.loadTextEditorViewState(input); this.notebookViewModel.restoreEditorViewState(viewState); + this.localStore.add(this.eventDispatcher.onDidChangeMetadata((e) => { + this.editorEditable?.set(e.source.editable); + })); + this.localStore.add(this.notebookViewModel.onDidChangeViewCells((e) => { if (e.synchronous) { e.splices.reverse().forEach((diff) => { @@ -361,7 +372,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { const scrollTop = this.list?.scrollTop || 0; const scrollHeight = this.list?.scrollHeight || 0; this.webview!.element.style.height = `${scrollHeight}px`; - let updateItems: { cell: CellViewModel, output: IOutput, cellTop: number }[] = []; + let updateItems: { cell: CodeCellViewModel, output: IOutput, cellTop: number }[] = []; if (this.webview?.insetMapping) { this.webview?.insetMapping.forEach((value, key) => { @@ -383,12 +394,6 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } })); - this.localStore.add(this.list!.onDidChangeFocus((e) => { - if (e.elements.length > 0) { - this.notebookService.updateNotebookActiveCell(input.viewType!, input.resource!, e.elements[0].handle); - } - })); - this.list?.splice(0, this.list?.length || 0); this.list?.splice(0, 0, this.notebookViewModel!.viewCells as CellViewModel[]); this.list?.layout(); @@ -415,6 +420,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { DOM.toggleClass(this.rootElement, 'narrow-width', dimension.width < 600); DOM.size(this.body, dimension.width, dimension.height); this.list?.layout(dimension.height, dimension.width); + this.eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); } protected saveState(): void { @@ -562,7 +568,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.list?.setFocus([insertIndex]); if (type === CellKind.Markdown) { - newCell.state = CellState.Editing; + newCell.editState = CellEditState.Editing; } DOM.scheduleAtNextAnimationFrame(() => { @@ -600,13 +606,13 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } editNotebookCell(cell: CellViewModel): void { - cell.state = CellState.Editing; + cell.editState = CellEditState.Editing; this.renderedEditors.get(cell)?.focus(); } saveNotebookCell(cell: ICellViewModel): void { - cell.state = CellState.Preview; + cell.editState = CellEditState.Preview; } getActiveCell() { @@ -619,6 +625,22 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { return undefined; } + async executeNotebookCell(cell: ICellViewModel): Promise { + try { + cell.runState = CellRunState.Running; + const provider = this.notebookService.getContributedNotebookProviders(cell.uri)[0]; + if (provider) { + const viewType = provider.id; + const notebookUri = CellUri.parse(cell.uri)?.notebook; + if (notebookUri) { + return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle); + } + } + } finally { + cell.runState = CellRunState.Idle; + } + } + focusNotebookCell(cell: ICellViewModel, focusEditor: boolean) { const index = this.notebookViewModel!.getViewCellIndex(cell); @@ -627,7 +649,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.list?.setSelection([index]); this.list?.focusView(); - cell.state = CellState.Editing; + cell.editState = CellEditState.Editing; cell.focusMode = CellFocusMode.Editor; this.revealInCenterIfOutsideViewport(cell); } else { @@ -636,7 +658,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { (document.activeElement as HTMLElement).blur(); } - cell.state = CellState.Preview; + cell.editState = CellEditState.Preview; cell.focusMode = CellFocusMode.Editor; this.list?.setFocus([index]); @@ -661,15 +683,12 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { fontInfo: this.fontInfo! }; } - getFontInfo(): BareFontInfo | undefined { - return this.fontInfo; - } triggerScroll(event: IMouseWheelEvent) { this.list?.triggerScrollFromMouseWheelEvent(event); } - createInset(cell: CellViewModel, output: IOutput, shadowContent: string, offset: number) { + createInset(cell: CodeCellViewModel, output: IOutput, shadowContent: string, offset: number) { if (!this.webview) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index 5d99148855..34b61144ca 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -53,7 +53,7 @@ export class NotebookEditorModel extends EditorModel { let notebook = this.getNotebook(); if (notebook) { - 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); this.notebook.insertNewCell(index, mainCell); this._dirty = true; this._onDidChangeDirty.fire(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookService.ts b/src/vs/workbench/contrib/notebook/browser/notebookService.ts index 4f78bf3861..34a89f713d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookService.ts @@ -26,11 +26,10 @@ export const INotebookService = createDecorator('notebookServi export interface IMainNotebookController { resolveNotebook(viewType: string, uri: URI): Promise; executeNotebook(viewType: string, uri: URI): Promise; - updateNotebookActiveCell(uri: URI, cellHandle: number): void; createRawCell(uri: URI, index: number, language: string, type: CellKind): Promise; deleteCell(uri: URI, index: number): Promise - executeNotebookActiveCell(uri: URI): Promise; onDidReceiveMessage(uri: URI, message: any): void; + executeNotebookCell(uri: URI, handle: number): Promise; destoryNotebookDocument(notebook: INotebookTextModel): Promise; save(uri: URI): Promise; } @@ -46,10 +45,10 @@ export interface INotebookService { getRendererInfo(handle: number): INotebookRendererInfo | undefined; resolveNotebook(viewType: string, uri: URI): Promise; executeNotebook(viewType: string, uri: URI): Promise; - executeNotebookActiveCell(viewType: string, uri: URI): Promise; + executeNotebookCell(viewType: string, uri: URI, handle: number): Promise; + getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[]; getNotebookProviderResourceRoots(): URI[]; - updateNotebookActiveCell(viewType: string, resource: URI, cellHandle: number): void; createNotebookCell(viewType: string, resource: URI, index: number, language: string, type: CellKind): Promise; deleteNotebookCell(viewType: string, resource: URI, index: number): Promise; destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void; @@ -242,14 +241,6 @@ export class NotebookService extends Disposable implements INotebookService { return modelData.model; } - updateNotebookActiveCell(viewType: string, resource: URI, cellHandle: number): void { - let provider = this._notebookProviders.get(viewType); - - if (provider) { - provider.controller.updateNotebookActiveCell(resource, cellHandle); - } - } - async createNotebookCell(viewType: string, resource: URI, index: number, language: string, type: CellKind): Promise { let provider = this._notebookProviders.get(viewType); @@ -280,11 +271,10 @@ export class NotebookService extends Disposable implements INotebookService { return; } - async executeNotebookActiveCell(viewType: string, uri: URI): Promise { - let provider = this._notebookProviders.get(viewType); - + async executeNotebookCell(viewType: string, uri: URI, handle: number): Promise { + const provider = this._notebookProviders.get(viewType); if (provider) { - await provider.controller.executeNotebookActiveCell(uri); + await provider.controller.executeNotebookCell(uri, handle); } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 4f0f1bba5a..a4cf215b35 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -18,7 +18,7 @@ import { NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/c import { Range } from 'vs/editor/common/core/range'; import { CellRevealType, CellRevealPosition, CursorAtBoundary } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; export class NotebookCellList extends WorkbenchList implements IDisposable { get onWillScroll(): Event { return this.view.onWillScroll; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 5c242eb94f..fee5bd313d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -14,7 +14,7 @@ import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebook import { IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; import { Emitter, Event } from 'vs/base/common/event'; @@ -76,7 +76,7 @@ let version = 0; export class BackLayerWebView extends Disposable { element: HTMLElement; webview: WebviewElement; - insetMapping: Map = new Map(); + insetMapping: Map = new Map(); reversedInsetMapping: Map = new Map(); preloadsCache: Map = new Map(); localResourceRootsCache: URI[] | undefined = undefined; @@ -278,7 +278,7 @@ export class BackLayerWebView extends Disposable { if (cell) { let outputIndex = cell.outputs.indexOf(output); cell.updateOutputHeight(outputIndex, outputHeight); - this.notebookEditor.layoutNotebookCell(cell, cell.getCellTotalHeight()); + this.notebookEditor.layoutNotebookCell(cell, cell.layoutInfo.totalHeight); } } else if (data.type === 'scroll-ack') { // const date = new Date(); @@ -305,12 +305,12 @@ export class BackLayerWebView extends Disposable { return webview; } - shouldUpdateInset(cell: CellViewModel, output: IOutput, cellTop: number) { + shouldUpdateInset(cell: CodeCellViewModel, output: IOutput, cellTop: number) { let outputCache = this.insetMapping.get(output)!; let outputIndex = cell.outputs.indexOf(output); let outputOffsetInOutputContainer = cell.getOutputOffset(outputIndex); - let outputOffset = cellTop + cell.editorHeight + 16 /* editor padding */ + 8 + outputOffsetInOutputContainer; + let outputOffset = cellTop + cell.layoutInfo.editorHeight + 16 /* editor padding */ + 8 + outputOffsetInOutputContainer; if (outputOffset === outputCache.cacheOffset) { return false; @@ -319,14 +319,14 @@ export class BackLayerWebView extends Disposable { return true; } - updateViewScrollTop(top: number, items: { cell: CellViewModel, output: IOutput, cellTop: number }[]) { + updateViewScrollTop(top: number, items: { cell: CodeCellViewModel, output: IOutput, cellTop: number }[]) { let widgets: IContentWidgetTopRequest[] = items.map(item => { let outputCache = this.insetMapping.get(item.output)!; let id = outputCache.outputId; let outputIndex = item.cell.outputs.indexOf(item.output); let outputOffsetInOutputContainer = item.cell.getOutputOffset(outputIndex); - let outputOffset = item.cellTop + item.cell.editorHeight + 16 /* editor padding */ + 16 + outputOffsetInOutputContainer; + let outputOffset = item.cellTop + item.cell.layoutInfo.editorHeight + 16 /* editor padding */ + 16 + outputOffsetInOutputContainer; outputCache.cacheOffset = outputOffset; return { @@ -345,7 +345,7 @@ export class BackLayerWebView extends Disposable { this.webview.sendMessage(message); } - createInset(cell: CellViewModel, output: IOutput, cellTop: number, offset: number, shadowContent: string, preloads: Set) { + createInset(cell: CodeCellViewModel, output: IOutput, cellTop: number, offset: number, shadowContent: string, preloads: Set) { this.updateRendererPreloads(preloads); let initialTop = cellTop + offset; let outputId = UUID.generateUuid(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts index fcd1f92184..fa34d52e5d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts @@ -3,20 +3,19 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable } from 'vs/base/common/lifecycle'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; import { IAction } from 'vs/base/common/actions'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -export class CellMenus implements IDisposable { +export class CellMenus { constructor( @IMenuService private readonly menuService: IMenuService, @IContextMenuService private readonly contextMenuService: IContextMenuService ) { } - getCellTitleActions(contextKeyService: IContextKeyService): IMenu { + getCellTitleMenu(contextKeyService: IContextKeyService): IMenu { return this.getMenu(MenuId.NotebookCellTitle, contextKeyService); } @@ -31,8 +30,4 @@ export class CellMenus implements IDisposable { return menu; } - - dispose(): void { - - } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index aa23dfbac1..d18cedd2d8 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -16,22 +16,24 @@ import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuItemAction, IMenu } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, NOTEBOOK_CELL_TYPE_CONTEXT_KEY } from 'vs/workbench/contrib/notebook/browser/constants'; -import { DeleteCellAction, EditCellAction, ExecuteCellAction, INotebookCellActionContext, InsertCodeCellBelowAction, MoveCellDownAction, MoveCellUpAction, SaveCellAction, InsertCodeCellAboveAction, InsertMarkdownCellAboveAction, InsertMarkdownCellBelowAction } from 'vs/workbench/contrib/notebook/browser/contrib/notebookActions'; -import { CellRenderTemplate, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, NOTEBOOK_CELL_TYPE_CONTEXT_KEY, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE_CONTEXT_KEY } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellRenderTemplate, CellRunState, ICellViewModel, INotebookEditor, CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell'; import { StatefullMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { CellViewModel } from '../../viewModel/notebookCellViewModel'; +import { INotebookCellActionContext, ExecuteCellAction } from 'vs/workbench/contrib/notebook/browser/contrib/notebookActions'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; const $ = DOM.$; @@ -114,56 +116,52 @@ abstract class AbstractCellRenderer { return toolbar; } - protected createMenu(): CellMenus { - const menu = this.instantiationService.createInstance(CellMenus); - return menu; + private getCellToolbarActions(menu: IMenu): IAction[] { + const actions: IAction[] = []; + for (let [, menuActions] of menu.getActions({ shouldForwardArgs: true })) { + actions.push(...menuActions); + } + + return actions; } - abstract getCellToolbarActions(element: CellViewModel): IAction[]; + protected setupCellToolbarActions(scopedContextKeyService: IContextKeyService, templateData: CellRenderTemplate, disposables: DisposableStore): void { + const cellMenu = this.instantiationService.createInstance(CellMenus); + const menu = disposables.add(cellMenu.getCellTitleMenu(scopedContextKeyService)); - showContextMenu(listIndex: number | undefined, element: CellViewModel, x: number, y: number) { - const actions: IAction[] = [ - this.instantiationService.createInstance(InsertCodeCellAboveAction), - this.instantiationService.createInstance(InsertCodeCellBelowAction), - this.instantiationService.createInstance(InsertMarkdownCellAboveAction), - this.instantiationService.createInstance(InsertMarkdownCellBelowAction), - ]; - actions.push(...this.getAdditionalContextMenuActions()); - actions.push(...[ - this.instantiationService.createInstance(DeleteCellAction) - ]); + const updateActions = () => { + const actions = this.getCellToolbarActions(menu); - this.contextMenuService.showContextMenu({ - getAnchor: () => { - return { - x, - y - }; - }, - getActions: () => actions, - getActionsContext: () => { - cell: element, - notebookEditor: this.notebookEditor - }, - autoSelectFirstItem: false - }); + templateData.toolbar.setActions(actions)(); + + if (templateData.focusIndicator) { + if (actions.length) { + templateData.focusIndicator.style.top = `24px`; + } else { + templateData.focusIndicator.style.top = `8px`; + } + } + }; + + updateActions(); + disposables.add(menu.onDidChange(() => { + updateActions(); + })); } - - abstract getAdditionalContextMenuActions(): IAction[]; } -export class MarkdownCellRenderer extends AbstractCellRenderer implements IListRenderer { +export class MarkdownCellRenderer extends AbstractCellRenderer implements IListRenderer { static readonly TEMPLATE_ID = 'markdown_cell'; private disposables: Map = new Map(); constructor( + contextKeyService: IContextKeyService, notehookEditor: INotebookEditor, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, @INotificationService notificationService: INotificationService, - @IContextKeyService contextKeyService: IContextKeyService ) { super(instantiationService, notehookEditor, contextMenuService, configurationService, keybindingService, notificationService, contextKeyService, 'markdown'); } @@ -194,11 +192,12 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR cellContainer: innerContent, editingContainer: codeInnerContent, disposables, - toolbar + toolbar, + toJSON: () => { return {}; } }; } - renderElement(element: CellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void { + renderElement(element: MarkdownCellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void { templateData.editingContainer!.style.display = 'none'; templateData.cellContainer.innerHTML = ''; let renderedHTML = element.getHTML(); @@ -211,22 +210,23 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR if (!this.disposables.has(element)) { this.disposables.set(element, new DisposableStore()); } - let elementDisposable = this.disposables.get(element); + const elementDisposable = this.disposables.get(element)!; - elementDisposable!.add(new StatefullMarkdownCell(this.notebookEditor, element, templateData, this.editorOptions, this.instantiationService)); + elementDisposable.add(new StatefullMarkdownCell(this.notebookEditor, element, templateData, this.editorOptions, this.instantiationService)); const contextKeyService = this.contextKeyService.createScoped(templateData.container); contextKeyService.createKey(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'markdown'); - const toolbarActions = this.getCellToolbarActions(element); - templateData.toolbar!.setActions(toolbarActions)(); + const cellEditableKey = contextKeyService.createKey(NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, !!(element.metadata?.editable)); + elementDisposable.add(element.onDidChangeMetadata((e) => { + cellEditableKey.set(!!e?.editable); + })); - if (templateData.focusIndicator) { - if (!toolbarActions.length) { - templateData.focusIndicator.style.top = `8px`; - } else { - templateData.focusIndicator.style.top = `24px`; - } - } + const editModeKey = contextKeyService.createKey(NOTEBOOK_CELL_MARKDOWN_EDIT_MODE_CONTEXT_KEY, element.editState === CellEditState.Editing); + elementDisposable.add(element.onDidChangeCellEditState(() => { + editModeKey.set(element.editState === CellEditState.Editing); + })); + + this.setupCellToolbarActions(contextKeyService, templateData, elementDisposable); } templateData.toolbar!.context = { @@ -236,51 +236,6 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR }; } - getCellToolbarActions(element: CellViewModel): IAction[] { - const viewModel = this.notebookEditor.viewModel; - - if (!viewModel) { - return []; - } - - const menu = this.createMenu().getCellTitleActions(this.contextKeyService); - const actions: IAction[] = []; - for (let [, actions] of menu.getActions({ shouldForwardArgs: true })) { - actions.push(...actions); - } - - const metadata = viewModel.metadata; - - if (!metadata || metadata.editable) { - actions.push( - this.instantiationService.createInstance(MoveCellUpAction), - this.instantiationService.createInstance(MoveCellDownAction), - this.instantiationService.createInstance(InsertCodeCellBelowAction) - ); - } - - const cellMetadata = element.metadata; - if (!cellMetadata || cellMetadata.editable) { - actions.push( - this.instantiationService.createInstance(EditCellAction), - this.instantiationService.createInstance(SaveCellAction) - ); - } - - if (!metadata || metadata.editable) { - this.instantiationService.createInstance(DeleteCellAction); - } - - return actions; - } - - getAdditionalContextMenuActions(): IAction[] { - return [ - this.instantiationService.createInstance(EditCellAction), - this.instantiationService.createInstance(SaveCellAction), - ]; - } - disposeTemplate(templateData: CellRenderTemplate): void { // throw nerendererw Error('Method not implemented.'); @@ -293,19 +248,19 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR } } -export class CodeCellRenderer extends AbstractCellRenderer implements IListRenderer { +export class CodeCellRenderer extends AbstractCellRenderer implements IListRenderer { static readonly TEMPLATE_ID = 'code_cell'; private disposables: Map = new Map(); constructor( protected notebookEditor: INotebookEditor, + protected contextKeyService: IContextKeyService, private renderedEditors: Map, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, @IInstantiationService instantiationService: IInstantiationService, @IKeybindingService keybindingService: IKeybindingService, @INotificationService notificationService: INotificationService, - @IContextKeyService contextKeyService: IContextKeyService ) { super(instantiationService, notebookEditor, contextMenuService, configurationService, keybindingService, notificationService, contextKeyService, 'python'); } @@ -317,12 +272,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende renderTemplate(container: HTMLElement): CellRenderTemplate { const disposables = new DisposableStore(); const toolbar = this.createToolbar(container); - toolbar.setActions([ - this.instantiationService.createInstance(MoveCellUpAction), - this.instantiationService.createInstance(MoveCellDownAction), - this.instantiationService.createInstance(InsertCodeCellBelowAction), - this.instantiationService.createInstance(DeleteCellAction) - ])(); disposables.add(toolbar); const cellContainer = DOM.append(container, $('.cell.code')); @@ -360,13 +309,15 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende focusIndicator, toolbar, runToolbar, + runButtonContainer, outputContainer, editor, - disposables + disposables, + toJSON: () => { return {}; } }; } - renderElement(element: CellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void { + renderElement(element: CodeCellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void { if (height === undefined) { return; } @@ -380,13 +331,21 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende this.disposables.set(element, new DisposableStore()); } - const elementDisposable = this.disposables.get(element); + const elementDisposable = this.disposables.get(element)!; - elementDisposable?.add(this.instantiationService.createInstance(CodeCell, this.notebookEditor, element, templateData)); + elementDisposable.add(this.instantiationService.createInstance(CodeCell, this.notebookEditor, element, templateData)); this.renderedEditors.set(element, templateData.editor); - elementDisposable?.add(element.onDidChangeTotalHeight(() => { - templateData.focusIndicator!.style.height = `${element.getIndicatorHeight()}px`; + elementDisposable.add(element.onDidChangeLayout(() => { + templateData.focusIndicator!.style.height = `${element.layoutInfo.indicatorHeight}px`; + })); + + elementDisposable.add(element.onDidChangeCellRunState(() => { + if (element.runState === CellRunState.Running) { + templateData.progressBar?.infinite().show(500); + } else { + templateData.progressBar?.hide(); + } })); const toolbarContext = { @@ -398,51 +357,14 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const contextKeyService = this.contextKeyService.createScoped(templateData.container); contextKeyService.createKey(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'code'); - const toolbarActions = this.getCellToolbarActions(element); - templateData.toolbar!.setActions(toolbarActions)(); - templateData.toolbar!.context = toolbarContext; + const cellEditableKey = contextKeyService.createKey(NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, !!(element.metadata?.editable)); + elementDisposable.add(element.onDidChangeMetadata((e) => { + cellEditableKey.set(!!e?.editable); + })); + + this.setupCellToolbarActions(contextKeyService, templateData, elementDisposable); + templateData.toolbar.context = toolbarContext; templateData.runToolbar!.context = toolbarContext; - - if (templateData.focusIndicator) { - if (!toolbarActions.length) { - templateData.focusIndicator.style.top = `8px`; - } else { - templateData.focusIndicator.style.top = `24px`; - } - } - } - - - getCellToolbarActions(element: CellViewModel): IAction[] { - const viewModel = this.notebookEditor.viewModel; - - if (!viewModel) { - return []; - } - - const menu = this.createMenu().getCellTitleActions(this.contextKeyService); - const actions: IAction[] = []; - for (let [, actions] of menu.getActions({ shouldForwardArgs: true })) { - actions.push(...actions); - } - - const metadata = viewModel.metadata; - - if (!metadata || metadata.editable) { - actions.push( - this.instantiationService.createInstance(MoveCellUpAction), - this.instantiationService.createInstance(MoveCellDownAction), - this.instantiationService.createInstance(InsertCodeCellBelowAction), - this.instantiationService.createInstance(DeleteCellAction) - ); - } - - return actions; - } - - - getAdditionalContextMenuActions(): IAction[] { - return []; } disposeTemplate(templateData: CellRenderTemplate): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index 85aea975de..835da45ab3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -13,8 +13,8 @@ import { raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; -import { CELL_MARGIN, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING, RUN_BUTTON_WIDTH } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; interface IMimeTypeRenderer extends IQuickPickItem { index: number; @@ -25,19 +25,16 @@ export class CodeCell extends Disposable { private outputElements = new Map(); constructor( private notebookEditor: INotebookEditor, - private viewCell: CellViewModel, + private viewCell: CodeCellViewModel, private templateData: CellRenderTemplate, @INotebookService private notebookService: INotebookService, @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(); - let width: number; - const listDimension = notebookEditor.getLayoutInfo(); - width = listDimension.width - CELL_MARGIN * 2 - RUN_BUTTON_WIDTH; - - const lineNum = viewCell.lineCount; - const lineHeight = notebookEditor.getLayoutInfo().fontInfo.lineHeight; + const width = this.viewCell.layoutInfo.editorWidth; + const lineNum = this.viewCell.lineCount; + const lineHeight = this.viewCell.layoutInfo.fontInfo?.lineHeight || 17; const totalHeight = lineNum * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; templateData.editor?.layout( { @@ -57,10 +54,8 @@ export class CodeCell extends Disposable { templateData.editor?.focus(); } - let realContentHeight = templateData.editor?.getContentHeight(); - let width: number; - const listDimension = notebookEditor.getLayoutInfo(); - width = listDimension.width - CELL_MARGIN * 2 - RUN_BUTTON_WIDTH; + const realContentHeight = templateData.editor?.getContentHeight(); + const width = this.viewCell.layoutInfo.editorWidth; if (realContentHeight !== undefined && realContentHeight !== totalHeight) { templateData.editor?.layout( @@ -85,6 +80,11 @@ export class CodeCell extends Disposable { } })); + templateData.editor?.updateOptions({ readOnly: !(viewCell.getEvaluatedMetadata(notebookEditor.viewModel?.metadata).editable) }); + this._register(viewCell.onDidChangeMetadata((e) => { + templateData.editor?.updateOptions({ readOnly: !(viewCell.getEvaluatedMetadata(notebookEditor.viewModel?.metadata).editable) }); + })); + let cellWidthResizeObserver = getResizesObserver(templateData.editorContainer!, { width: width, height: totalHeight @@ -106,7 +106,7 @@ export class CodeCell extends Disposable { this._register(templateData.editor!.onDidContentSizeChange((e) => { if (e.contentHeightChanged) { - if (this.viewCell.editorHeight !== e.contentHeight) { + if (this.viewCell.layoutInfo.editorHeight !== e.contentHeight) { let viewLayout = templateData.editor!.getLayoutInfo(); templateData.editor?.layout( @@ -256,7 +256,7 @@ export class CodeCell extends Disposable { if (result.shadowContent) { this.viewCell.selfSizeMonitoring = true; - let editorHeight = this.viewCell.editorHeight; + let editorHeight = this.viewCell.layoutInfo.editorHeight; this.notebookEditor.createInset(this.viewCell, currOutput, result.shadowContent, editorHeight + 8 + this.viewCell.getOutputOffset(index)); } else { DOM.addClass(outputItemDiv, 'foreground'); @@ -266,11 +266,10 @@ export class CodeCell extends Disposable { if (hasDynamicHeight) { let clientHeight = outputItemDiv.clientHeight; - let listDimension = this.notebookEditor.getLayoutInfo(); - let dimension = listDimension ? { - width: listDimension.width - CELL_MARGIN * 2 - RUN_BUTTON_WIDTH, + let dimension = { + width: this.viewCell.layoutInfo.editorWidth, height: clientHeight - } : undefined; + }; const elementSizeObserver = getResizesObserver(outputItemDiv, dimension, () => { if (this.templateData.outputContainer && document.body.contains(this.templateData.outputContainer!)) { let height = elementSizeObserver.getHeight() + 8 * 2; // include padding @@ -369,7 +368,7 @@ export class CodeCell extends Disposable { } relayoutCell() { - this.notebookEditor.layoutNotebookCell(this.viewCell, this.viewCell.getCellTotalHeight()); + this.notebookEditor.layoutNotebookCell(this.viewCell, this.viewCell.layoutInfo.totalHeight); } dispose() { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index 9af57ee1b6..ab75c8c7bf 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -8,11 +8,11 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; -import { INotebookEditor, CellRenderTemplate, CellFocusMode, CellState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, CellRenderTemplate, CellFocusMode, CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { raceCancellation } from 'vs/base/common/async'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; -import { CELL_MARGIN, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; +import { EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; export class StatefullMarkdownCell extends Disposable { private editor: CodeEditorWidget | null = null; @@ -23,7 +23,7 @@ export class StatefullMarkdownCell extends Disposable { constructor( notebookEditor: INotebookEditor, - public viewCell: CellViewModel, + public viewCell: MarkdownCellViewModel, templateData: CellRenderTemplate, editorOptions: IEditorOptions, instantiationService: IInstantiationService @@ -36,18 +36,11 @@ export class StatefullMarkdownCell extends Disposable { this._register(this.localDisposables); const viewUpdate = () => { - if (viewCell.state === CellState.Editing) { + if (viewCell.editState === CellEditState.Editing) { // switch to editing mode - let width: number; - const listDimension = notebookEditor.getLayoutInfo(); - width = listDimension.width - CELL_MARGIN * 2; - // if (listDimension) { - // } else { - // width = this.cellContainer.clientWidth - 24 /** for scrollbar and margin right */; - // } - + let width = viewCell.layoutInfo.editorWidth; const lineNum = viewCell.lineCount; - const lineHeight = notebookEditor.getLayoutInfo().fontInfo.lineHeight; + const lineHeight = viewCell.layoutInfo.fontInfo?.lineHeight || 17; const totalHeight = Math.max(lineNum, 1) * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; if (this.editor) { @@ -106,7 +99,7 @@ export class StatefullMarkdownCell extends Disposable { notebookEditor.layoutNotebookCell(viewCell, this.editor!.getContentHeight() + 32 + clientHeight); })); - if (viewCell.state === CellState.Editing) { + if (viewCell.editState === CellEditState.Editing) { this.editor!.focus(); } }); @@ -197,7 +190,7 @@ export class StatefullMarkdownCell extends Disposable { } }; - this._register(viewCell.onDidChangeCellState(() => { + this._register(viewCell.onDidChangeCellEditState(() => { this.localDisposables.clear(); viewUpdate(); })); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts new file mode 100644 index 0000000000..182dd1c15e --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -0,0 +1,339 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import * as model from 'vs/editor/common/model'; +import { Range } from 'vs/editor/common/core/range'; +import { ICell, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CursorAtBoundary, CellFocusMode, CellEditState, CellRunState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { EDITOR_TOP_PADDING, EDITOR_TOOLBAR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; +import { SearchParams } from 'vs/editor/common/model/textModelSearch'; + +export const NotebookCellMetadataDefaults = { + editable: true, + runnable: true +}; + +export abstract class BaseCellViewModel extends Disposable { + protected readonly _onDidDispose = new Emitter(); + readonly onDidDispose = this._onDidDispose.event; + protected readonly _onDidChangeCellEditState = new Emitter(); + readonly onDidChangeCellEditState = this._onDidChangeCellEditState.event; + protected readonly _onDidChangeCellRunState = new Emitter(); + readonly onDidChangeCellRunState = this._onDidChangeCellRunState.event; + protected readonly _onDidChangeFocusMode = new Emitter(); + readonly onDidChangeFocusMode = this._onDidChangeFocusMode.event; + protected readonly _onDidChangeEditorAttachState = new Emitter(); + readonly onDidChangeEditorAttachState = this._onDidChangeEditorAttachState.event; + protected readonly _onDidChangeCursorSelection: Emitter = this._register(new Emitter()); + public readonly onDidChangeCursorSelection: Event = this._onDidChangeCursorSelection.event; + protected readonly _onDidChangeMetadata: Emitter = this._register(new Emitter()); + public readonly onDidChangeMetadata: Event = this._onDidChangeMetadata.event; + get handle() { + return this.cell.handle; + } + get uri() { + return this.cell.uri; + } + get lineCount() { + return this.cell.source.length; + } + get metadata() { + return this.cell.metadata; + } + + private _editState: CellEditState = CellEditState.Preview; + + get editState(): CellEditState { + return this._editState; + } + + set editState(newState: CellEditState) { + if (newState === this._editState) { + return; + } + + this._editState = newState; + this._onDidChangeCellEditState.fire(); + } + private _runState: CellRunState = CellRunState.Idle; + + get runState(): CellRunState { + return this._runState; + } + + set runState(newState: CellRunState) { + if (newState === this._runState) { + return; + } + + this._runState = newState; + this._onDidChangeCellRunState.fire(); + } + private _focusMode: CellFocusMode = CellFocusMode.Container; + get focusMode() { + return this._focusMode; + } + set focusMode(newMode: CellFocusMode) { + this._focusMode = newMode; + this._onDidChangeFocusMode.fire(); + } + + protected _textEditor?: ICodeEditor; + get editorAttached(): boolean { + return !!this._textEditor; + } + private _cursorChangeListener: IDisposable | null = null; + private _editorViewStates: editorCommon.ICodeEditorViewState | null = null; + private _resolvedDecorations = new Map(); + private _lastDecorationId: number = 0; + protected _textModel?: model.ITextModel; + + constructor(readonly viewType: string, readonly notebookHandle: number, readonly cell: ICell, public id: string) { + super(); + + this._register(cell.onDidChangeMetadata((e) => { + this._onDidChangeMetadata.fire(e); + })); + } + + abstract hasDynamicHeight(): boolean; + abstract getHeight(lineHeight: number): number; + abstract onDeselect(): void; + + assertTextModelAttached(): boolean { + if (this._textModel && this._textEditor && this._textEditor.getModel() === this._textModel) { + return true; + } + + return false; + } + + attachTextEditor(editor: ICodeEditor) { + if (!editor.hasModel()) { + throw new Error('Invalid editor: model is missing'); + } + + if (this._textEditor === editor) { + if (this._cursorChangeListener === null) { + this._cursorChangeListener = this._textEditor.onDidChangeCursorSelection(() => this._onDidChangeCursorSelection.fire()); + this._onDidChangeCursorSelection.fire(); + } + return; + } + + this._textEditor = editor; + + if (this._editorViewStates) { + this.restoreViewState(this._editorViewStates); + } + + this._resolvedDecorations.forEach((value, key) => { + if (key.startsWith('_lazy_')) { + // lazy ones + const ret = this._textEditor!.deltaDecorations([], [value.options]); + this._resolvedDecorations.get(key)!.id = ret[0]; + } + else { + const ret = this._textEditor!.deltaDecorations([], [value.options]); + this._resolvedDecorations.get(key)!.id = ret[0]; + } + }); + + this._cursorChangeListener = this._textEditor.onDidChangeCursorSelection(() => this._onDidChangeCursorSelection.fire()); + this._onDidChangeCursorSelection.fire(); + this._onDidChangeEditorAttachState.fire(true); + } + + detachTextEditor() { + this._editorViewStates = this.saveViewState(); + // decorations need to be cleared first as editors can be resued. + this._resolvedDecorations.forEach(value => { + let resolvedid = value.id; + + if (resolvedid) { + this._textEditor?.deltaDecorations([resolvedid], []); + } + }); + + this._textEditor = undefined; + this._cursorChangeListener?.dispose(); + this._cursorChangeListener = null; + this._onDidChangeEditorAttachState.fire(false); + } + + getText(): string { + if (this._textModel) { + return this._textModel.getValue(); + } + + return this.cell.source.join('\n'); + } + + private saveViewState(): editorCommon.ICodeEditorViewState | null { + if (!this._textEditor) { + return null; + } + + return this._textEditor.saveViewState(); + } + + saveEditorViewState() { + if (this._textEditor) { + this._editorViewStates = this.saveViewState(); + } + + return this._editorViewStates; + } + + restoreEditorViewState(editorViewStates: editorCommon.ICodeEditorViewState | null) { + this._editorViewStates = editorViewStates; + } + + private restoreViewState(state: editorCommon.ICodeEditorViewState | null): void { + if (state) { + this._textEditor?.restoreViewState(state); + } + } + + addDecoration(decoration: model.IModelDeltaDecoration): string { + if (!this._textEditor) { + const id = ++this._lastDecorationId; + const decorationId = `_lazy_${this.id};${id}`; + this._resolvedDecorations.set(decorationId, { options: decoration }); + return decorationId; + } + + const result = this._textEditor.deltaDecorations([], [decoration]); + this._resolvedDecorations.set(result[0], { id: result[0], options: decoration }); + return result[0]; + } + + removeDecoration(decorationId: string) { + const realDecorationId = this._resolvedDecorations.get(decorationId); + + if (this._textEditor && realDecorationId && realDecorationId.id !== undefined) { + this._textEditor.deltaDecorations([realDecorationId.id!], []); + } + + // lastly, remove all the cache + this._resolvedDecorations.delete(decorationId); + } + + deltaDecorations(oldDecorations: string[], newDecorations: model.IModelDeltaDecoration[]): string[] { + oldDecorations.forEach(id => { + this.removeDecoration(id); + }); + + const ret = newDecorations.map(option => { + return this.addDecoration(option); + }); + + return ret; + } + + revealRangeInCenter(range: Range) { + this._textEditor?.revealRangeInCenter(range, editorCommon.ScrollType.Immediate); + } + + setSelection(range: Range) { + this._textEditor?.setSelection(range); + } + + getLineScrollTopOffset(line: number): number { + if (!this._textEditor) { + return 0; + } + + return this._textEditor.getTopForLineNumber(line) + EDITOR_TOP_PADDING + EDITOR_TOOLBAR_HEIGHT; + } + + cursorAtBoundary(): CursorAtBoundary { + if (!this._textEditor) { + return CursorAtBoundary.None; + } + + // only validate primary cursor + const selection = this._textEditor.getSelection(); + + // only validate empty cursor + if (!selection || !selection.isEmpty()) { + return CursorAtBoundary.None; + } + + // we don't allow attaching text editor without a model + const lineCnt = this._textEditor.getModel()!.getLineCount(); + + if (selection.startLineNumber === lineCnt) { + // bottom + if (selection.startLineNumber === 1) { + return CursorAtBoundary.Both; + } + else { + return CursorAtBoundary.Bottom; + } + } + + if (selection.startLineNumber === 1) { + return CursorAtBoundary.Top; + } + + return CursorAtBoundary.None; + } + + protected _buffer: model.ITextBuffer | null = null; + + protected cellStartFind(value: string): model.FindMatch[] | null { + let cellMatches: model.FindMatch[] = []; + + if (this.assertTextModelAttached()) { + cellMatches = this._textModel!.findMatches(value, false, false, false, null, false); + } else { + if (!this._buffer) { + this._buffer = this.cell.resolveTextBufferFactory().create(model.DefaultEndOfLine.LF); + } + + const lineCount = this._buffer.getLineCount(); + const fullRange = new Range(1, 1, lineCount, this._buffer.getLineLength(lineCount) + 1); + const searchParams = new SearchParams(value, false, false, null); + const searchData = searchParams.parseSearchRequest(); + + if (!searchData) { + return null; + } + + cellMatches = this._buffer.findMatchesLineByLine(fullRange, searchData, false, 1000); + } + + return cellMatches; + } + + getEvaluatedMetadata(documentMetadata: NotebookDocumentMetadata | undefined): NotebookCellMetadata { + const editable: boolean = this.metadata?.editable === undefined + ? (documentMetadata?.cellEditable === undefined ? NotebookCellMetadataDefaults.editable : documentMetadata?.cellEditable) + : this.metadata?.editable; + + const runnable: boolean = this.metadata?.runnable === undefined + ? (documentMetadata?.cellRunnable === undefined ? NotebookCellMetadataDefaults.runnable : documentMetadata?.cellRunnable) + : this.metadata?.runnable; + + return { + editable, + runnable + }; + } + + toJSON(): any { + return { + handle: this.handle + }; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts index 7f7313f249..889d37467d 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts @@ -6,18 +6,16 @@ import { ICell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; - +import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; /** * It should not modify Undo/Redo stack */ export interface ICellEditingDelegate { - insertCell?(index: number, viewCell: CellViewModel): void; + insertCell?(index: number, viewCell: BaseCellViewModel): void; deleteCell?(index: number, cell: ICell): void; moveCell?(fromIndex: number, toIndex: number): void; + createCellViewModel?(cell: ICell): BaseCellViewModel; } export class InsertCellEdit implements IResourceUndoRedoElement { @@ -26,7 +24,7 @@ export class InsertCellEdit implements IResourceUndoRedoElement { constructor( public resource: URI, private insertIndex: number, - private cell: CellViewModel, + private cell: BaseCellViewModel, private editingDelegate: ICellEditingDelegate ) { } @@ -55,10 +53,8 @@ export class DeleteCellEdit implements IResourceUndoRedoElement { constructor( public resource: URI, private insertIndex: number, - cell: CellViewModel, - private editingDelegate: ICellEditingDelegate, - private instantiationService: IInstantiationService, - private notebookViewModel: NotebookViewModel + cell: BaseCellViewModel, + private editingDelegate: ICellEditingDelegate ) { this._rawCell = cell.cell; @@ -67,11 +63,11 @@ export class DeleteCellEdit implements IResourceUndoRedoElement { } undo(): void | Promise { - if (!this.editingDelegate.insertCell) { + if (!this.editingDelegate.insertCell || !this.editingDelegate.createCellViewModel) { throw new Error('Notebook Insert Cell not implemented for Undo/Redo'); } - const cell = this.instantiationService.createInstance(CellViewModel, this.notebookViewModel.viewType, this.notebookViewModel.handle, this._rawCell); + const cell = this.editingDelegate.createCellViewModel(this._rawCell); this.editingDelegate.insertCell(this.insertIndex, cell); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts new file mode 100644 index 0000000000..140caec0ec --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -0,0 +1,229 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import * as UUID from 'vs/base/common/uuid'; +import * as model from 'vs/editor/common/model'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; +import { EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, CELL_MARGIN, RUN_BUTTON_WIDTH } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellEditState, ICellViewModel, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellKind, ICell, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { BaseCellViewModel } from './baseCellViewModel'; +import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; + +export class CodeCellViewModel extends BaseCellViewModel implements ICellViewModel { + cellKind: CellKind.Code = CellKind.Code; + protected readonly _onDidChangeOutputs = new Emitter(); + readonly onDidChangeOutputs = this._onDidChangeOutputs.event; + private _outputCollection: number[] = []; + private _selfSizeMonitoring: boolean = false; + set selfSizeMonitoring(newVal: boolean) { + this._selfSizeMonitoring = newVal; + } + + get selfSizeMonitoring() { + return this._selfSizeMonitoring; + } + + private _outputsTop: PrefixSumComputer | null = null; + get outputs() { + return this.cell.outputs; + } + + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + public readonly onDidChangeContent: Event = this._onDidChangeContent.event; + + protected readonly _onDidChangeLayout = new Emitter(); + readonly onDidChangeLayout = this._onDidChangeLayout.event; + + private _editorHeight = 0; + set editorHeight(height: number) { + this._editorHeight = height; + + this.layoutChange({ editorHeight: true }); + } + + get editorHeight() { + return this._editorHeight; + } + + private _layoutInfo: CodeCellLayoutInfo; + + get layoutInfo() { + return this._layoutInfo; + } + + constructor( + readonly viewType: string, + readonly notebookHandle: number, + readonly cell: ICell, + readonly eventDispatcher: NotebookEventDispatcher, + @ITextModelService private readonly _modelService: ITextModelService, + ) { + super(viewType, notebookHandle, cell, UUID.generateUuid()); + if (this.cell.onDidChangeOutputs) { + this._register(this.cell.onDidChangeOutputs((splices) => { + this._outputCollection = new Array(this.cell.outputs.length); + this._outputsTop = null; + this._onDidChangeOutputs.fire(splices); + })); + } + + this._outputCollection = new Array(this.cell.outputs.length); + this._buffer = null; + + this._layoutInfo = { + fontInfo: null, + editorHeight: 0, + editorWidth: 0, + outputTotalHeight: 0, + totalHeight: 0, + indicatorHeight: 0 + }; + + this._register(eventDispatcher.onDidChangeLayout((e) => { + if (e.source.width !== undefined) { + this.layoutChange({ outerWidth: e.value.width, font: e.value.fontInfo }); + } + })); + } + + layoutChange(state: CodeCellLayoutChangeEvent) { + // recompute + this._ensureOutputsTop(); + const outputTotalHeight = this._outputsTop!.getTotalValue(); + const totalHeight = this.outputs.length + ? EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING + 16 + outputTotalHeight + : EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING + outputTotalHeight; + const indicatorHeight = totalHeight - EDITOR_TOOLBAR_HEIGHT - 16; + const editorWidth = state.outerWidth !== undefined ? state.outerWidth - CELL_MARGIN * 2 - RUN_BUTTON_WIDTH : 0; + this._layoutInfo = { + fontInfo: state.font || null, + editorHeight: this._editorHeight, + editorWidth, + outputTotalHeight, + totalHeight, + indicatorHeight + }; + + if (state.editorHeight || state.outputHeight) { + state.totalHeight = true; + } + + this._onDidChangeLayout.fire(state); + } + + hasDynamicHeight() { + if (this.selfSizeMonitoring) { + // if there is an output rendered in the webview, it should always be false + return false; + } + + if (this.outputs && this.outputs.length > 0) { + // if it contains output, it will be marked as dynamic height + // thus when it's being rendered, the list view will `probeHeight` + // inside which, we will check domNode's height directly instead of doing another `renderElement` with height undefined. + return true; + } + else { + return false; + } + } + + getHeight(lineHeight: number) { + return this.lineCount * lineHeight + 16 + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + } + + save() { + if (this._textModel && !this._textModel.isDisposed() && this.editState === CellEditState.Editing) { + let cnt = this._textModel.getLineCount(); + this.cell.source = this._textModel.getLinesContent().map((str, index) => str + (index !== cnt - 1 ? '\n' : '')); + } + } + + async resolveTextModel(): Promise { + if (!this._textModel) { + const ref = await this._modelService.createModelReference(this.cell.uri); + this._textModel = ref.object.textEditorModel; + this._buffer = this._textModel.getTextBuffer(); + this._register(ref); + this._register(this._textModel.onDidChangeContent(() => { + this.cell.contentChange(); + this._onDidChangeContent.fire(); + })); + } + + return this._textModel; + } + + onDeselect() { + this.editState = CellEditState.Preview; + } + + updateOutputHeight(index: number, height: number) { + if (index >= this._outputCollection.length) { + throw new Error('Output index out of range!'); + } + + this._outputCollection[index] = height; + this._ensureOutputsTop(); + this._outputsTop!.changeValue(index, height); + this.layoutChange({ outputHeight: true }); + } + + getOutputOffset(index: number): number { + if (index >= this._outputCollection.length) { + throw new Error('Output index out of range!'); + } + + this._ensureOutputsTop(); + + return this._outputsTop!.getAccumulatedValue(index - 1); + } + + spliceOutputHeights(start: number, deleteCnt: number, heights: number[]) { + this._ensureOutputsTop(); + + this._outputsTop!.removeValues(start, deleteCnt); + if (heights.length) { + const values = new Uint32Array(heights.length); + for (let i = 0; i < heights.length; i++) { + values[i] = heights[i]; + } + + this._outputsTop!.insertValues(start, values); + } + + this.layoutChange({ outputHeight: true }); + } + + private _ensureOutputsTop(): void { + if (!this._outputsTop) { + const values = new Uint32Array(this._outputCollection.length); + for (let i = 0; i < this._outputCollection.length; i++) { + values[i] = this._outputCollection[i]; + } + + this._outputsTop = new PrefixSumComputer(values); + } + } + + private readonly _hasFindResult = this._register(new Emitter()); + public readonly hasFindResult: Event = this._hasFindResult.event; + + startFind(value: string): CellFindMatch | null { + const matches = super.cellStartFind(value); + + if (matches === null) { + return null; + } + + return { + cell: this, + matches + }; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts new file mode 100644 index 0000000000..e8308c3995 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookLayoutChangeEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +export enum NotebookViewEventType { + LayoutChanged = 1, + MetadataChanged = 2 +} + +export class NotebookLayoutChangedEvent { + public readonly type = NotebookViewEventType.LayoutChanged; + + constructor(readonly source: NotebookLayoutChangeEvent, readonly value: NotebookLayoutInfo) { + + } +} + + +export class NotebookMetadataChangedEvent { + public readonly type = NotebookViewEventType.MetadataChanged; + + constructor(readonly source: NotebookDocumentMetadata) { + + } +} + + +export type NotebookViewEvent = NotebookLayoutChangedEvent | NotebookMetadataChangedEvent; + +export class NotebookEventDispatcher { + protected readonly _onDidChangeLayout = new Emitter(); + readonly onDidChangeLayout = this._onDidChangeLayout.event; + protected readonly _onDidChangeMetadata = new Emitter(); + readonly onDidChangeMetadata = this._onDidChangeMetadata.event; + + constructor() { + } + + emit(events: NotebookViewEvent[]) { + for (let i = 0, len = events.length; i < len; i++) { + let e = events[i]; + + switch (e.type) { + case NotebookViewEventType.LayoutChanged: + this._onDidChangeLayout.fire(e); + break; + case NotebookViewEventType.MetadataChanged: + this._onDidChangeMetadata.fire(e); + break; + } + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts new file mode 100644 index 0000000000..14c1f8f6c6 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import * as UUID from 'vs/base/common/uuid'; +import * as model from 'vs/editor/common/model'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ICellViewModel, CellFindMatch, MarkdownCellLayoutInfo, MarkdownCellLayoutChangeEvent, CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; +import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; +import { CellKind, ICell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; +import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; + +export class MarkdownCellViewModel extends BaseCellViewModel implements ICellViewModel { + cellKind: CellKind.Markdown = CellKind.Markdown; + private _mdRenderer: MarkdownRenderer | null = null; + private _html: HTMLElement | null = null; + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + public readonly onDidChangeContent: Event = this._onDidChangeContent.event; + + private _layoutInfo: MarkdownCellLayoutInfo; + + get layoutInfo() { + return this._layoutInfo; + } + + protected readonly _onDidChangeLayout = new Emitter(); + readonly onDidChangeLayout = this._onDidChangeLayout.event; + + constructor( + readonly viewType: string, + readonly notebookHandle: number, + readonly cell: ICell, + readonly eventDispatcher: NotebookEventDispatcher, + @IInstantiationService private readonly _instaService: IInstantiationService, + @ITextModelService private readonly _modelService: ITextModelService) { + super(viewType, notebookHandle, cell, UUID.generateUuid()); + + this._layoutInfo = { + fontInfo: null, + editorWidth: 0 + }; + + this._register(eventDispatcher.onDidChangeLayout((e) => { + if (e.source.width || e.source.fontInfo) { + this.layoutChange({ outerWidth: e.value.width, font: e.value.fontInfo }); + } + })); + } + + layoutChange(state: MarkdownCellLayoutChangeEvent) { + // recompute + const editorWidth = state.outerWidth !== undefined ? state.outerWidth - CELL_MARGIN * 2 : 0; + + this._layoutInfo = { + fontInfo: state.font || null, + editorWidth + }; + + this._onDidChangeLayout.fire(state); + } + + hasDynamicHeight() { + return true; + } + + getHeight(lineHeight: number) { + return 100; + } + + setText(strs: string[]) { + this.cell.source = strs; + this._html = null; + } + + save() { + if (this._textModel && !this._textModel.isDisposed() && this.editState === CellEditState.Editing) { + let cnt = this._textModel.getLineCount(); + this.cell.source = this._textModel.getLinesContent().map((str, index) => str + (index !== cnt - 1 ? '\n' : '')); + } + } + + getHTML(): HTMLElement | null { + if (this.cellKind === CellKind.Markdown) { + if (this._html) { + return this._html; + } + let renderer = this.getMarkdownRenderer(); + this._html = renderer.render({ value: this.getText(), isTrusted: true }).element; + return this._html; + } + return null; + } + + async resolveTextModel(): Promise { + if (!this._textModel) { + const ref = await this._modelService.createModelReference(this.cell.uri); + this._textModel = ref.object.textEditorModel; + this._buffer = this._textModel.getTextBuffer(); + this._register(ref); + this._register(this._textModel.onDidChangeContent(() => { + this.cell.contentChange(); + this._html = null; + this._onDidChangeContent.fire(); + })); + } + return this._textModel; + } + + onDeselect() { + this.editState = CellEditState.Preview; + } + + getMarkdownRenderer() { + if (!this._mdRenderer) { + this._mdRenderer = this._instaService.createInstance(MarkdownRenderer); + } + return this._mdRenderer; + } + + private readonly _hasFindResult = this._register(new Emitter()); + public readonly hasFindResult: Event = this._hasFindResult.event; + + startFind(value: string): CellFindMatch | null { + const matches = super.cellStartFind(value); + + if (matches === null) { + return null; + } + + return { + cell: this, + matches + }; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts deleted file mode 100644 index e783b3c71c..0000000000 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts +++ /dev/null @@ -1,532 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import * as UUID from 'vs/base/common/uuid'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { Range } from 'vs/editor/common/core/range'; -import * as editorCommon from 'vs/editor/common/editorCommon'; -import * as model from 'vs/editor/common/model'; -import { SearchParams } from 'vs/editor/common/model/textModelSearch'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; -import { CellKind, ICell, IOutput, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { CellFindMatch, CellState, CursorAtBoundary, CellFocusMode, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; - -export class CellViewModel extends Disposable implements ICellViewModel { - - private _mdRenderer: MarkdownRenderer | null = null; - private _html: HTMLElement | null = null; - protected readonly _onDidDispose = new Emitter(); - readonly onDidDispose = this._onDidDispose.event; - protected readonly _onDidChangeCellState = new Emitter(); - readonly onDidChangeCellState = this._onDidChangeCellState.event; - protected readonly _onDidChangeFocusMode = new Emitter(); - readonly onDidChangeFocusMode = this._onDidChangeFocusMode.event; - protected readonly _onDidChangeOutputs = new Emitter(); - readonly onDidChangeOutputs = this._onDidChangeOutputs.event; - - protected readonly _onDidChangeTotalHeight = new Emitter(); - readonly onDidChangeTotalHeight = this._onDidChangeTotalHeight.event; - private _outputCollection: number[] = []; - protected _outputsTop: PrefixSumComputer | null = null; - - get handle() { - return this.cell.handle; - } - - get uri() { - return this.cell.uri; - } - - get cellKind() { - return this.cell.cellKind; - } - get lineCount() { - return this.cell.source.length; - } - get outputs() { - return this.cell.outputs; - } - - get metadata() { - return this.cell.metadata; - } - - private _state: CellState = CellState.Preview; - - get state(): CellState { - return this._state; - } - - set state(newState: CellState) { - if (newState === this._state) { - return; - } - - this._state = newState; - this._onDidChangeCellState.fire(); - } - - private _focusMode: CellFocusMode = CellFocusMode.Container; - - get focusMode() { - return this._focusMode; - } - - set focusMode(newMode: CellFocusMode) { - this._focusMode = newMode; - this._onDidChangeFocusMode.fire(); - } - - private _selfSizeMonitoring: boolean = false; - - set selfSizeMonitoring(newVal: boolean) { - this._selfSizeMonitoring = newVal; - } - - get selfSizeMonitoring() { - return this._selfSizeMonitoring; - } - - private _editorHeight = 0; - set editorHeight(height: number) { - this._editorHeight = height; - this._onDidChangeTotalHeight.fire(); - } - - get editorHeight(): number { - return this._editorHeight; - } - - protected readonly _onDidChangeEditorAttachState = new Emitter(); - readonly onDidChangeEditorAttachState = this._onDidChangeEditorAttachState.event; - - get editorAttached(): boolean { - return !!this._textEditor; - } - - private _textModel?: model.ITextModel; - private _textEditor?: ICodeEditor; - private _buffer: model.ITextBuffer | null; - private _editorViewStates: editorCommon.ICodeEditorViewState | null; - private _lastDecorationId: number = 0; - private _resolvedDecorations = new Map(); - private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); - public readonly onDidChangeContent: Event = this._onDidChangeContent.event; - private readonly _onDidChangeCursorSelection: Emitter = this._register(new Emitter()); - public readonly onDidChangeCursorSelection: Event = this._onDidChangeCursorSelection.event; - - private _cursorChangeListener: IDisposable | null = null; - - readonly id: string = UUID.generateUuid(); - - constructor( - readonly viewType: string, - readonly notebookHandle: number, - readonly cell: ICell, - @IInstantiationService private readonly _instaService: IInstantiationService, - @ITextModelService private readonly _modelService: ITextModelService, - ) { - super(); - if (this.cell.onDidChangeOutputs) { - this._register(this.cell.onDidChangeOutputs((splices) => { - this._outputCollection = new Array(this.cell.outputs.length); - this._outputsTop = null; - this._onDidChangeOutputs.fire(splices); - })); - } - - this._outputCollection = new Array(this.cell.outputs.length); - this._buffer = null; - this._editorViewStates = null; - } - - restoreEditorViewState(editorViewStates: editorCommon.ICodeEditorViewState | null) { - this._editorViewStates = editorViewStates; - } - - saveEditorViewState() { - if (this._textEditor) { - this._editorViewStates = this.saveViewState(); - } - - return this._editorViewStates; - } - - - //#region Search - private readonly _hasFindResult = this._register(new Emitter()); - public readonly hasFindResult: Event = this._hasFindResult.event; - - startFind(value: string): CellFindMatch | null { - let cellMatches: model.FindMatch[] = []; - - if (this.assertTextModelAttached()) { - cellMatches = this._textModel!.findMatches(value, false, false, false, null, false); - } else { - if (!this._buffer) { - this._buffer = this.cell.resolveTextBufferFactory().create(model.DefaultEndOfLine.LF); - } - - const lineCount = this._buffer.getLineCount(); - const fullRange = new Range(1, 1, lineCount, this._buffer.getLineLength(lineCount) + 1); - const searchParams = new SearchParams(value, false, false, null); - const searchData = searchParams.parseSearchRequest(); - - if (!searchData) { - return null; - } - - cellMatches = this._buffer.findMatchesLineByLine(fullRange, searchData, false, 1000); - } - - return { - cell: this, - matches: cellMatches - }; - } - - assertTextModelAttached(): boolean { - if (this._textModel && this._textEditor && this._textEditor.getModel() === this._textModel) { - return true; - } - - return false; - } - - private saveViewState(): editorCommon.ICodeEditorViewState | null { - if (!this._textEditor) { - return null; - } - - return this._textEditor.saveViewState(); - } - - - private restoreViewState(state: editorCommon.ICodeEditorViewState | null): void { - if (state) { - this._textEditor?.restoreViewState(state); - } - } - - //#endregion - - hasDynamicHeight() { - if (this.selfSizeMonitoring) { - // if there is an output rendered in the webview, it should always be false - return false; - } - - if (this.cellKind === CellKind.Code) { - if (this.outputs && this.outputs.length > 0) { - // if it contains output, it will be marked as dynamic height - // thus when it's being rendered, the list view will `probeHeight` - // inside which, we will check domNode's height directly instead of doing another `renderElement` with height undefined. - return true; - } - else { - return false; - } - } - - return true; - } - - getHeight(lineHeight: number) { - if (this.cellKind === CellKind.Markdown) { - return 100; - } - else { - return this.lineCount * lineHeight + 16 + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; - } - } - setText(strs: string[]) { - this.cell.source = strs; - this._html = null; - } - - save() { - if (this._textModel && !this._textModel.isDisposed() && this.state === CellState.Editing) { - let cnt = this._textModel.getLineCount(); - this.cell.source = this._textModel.getLinesContent().map((str, index) => str + (index !== cnt - 1 ? '\n' : '')); - } - } - getText(): string { - if (this._textModel) { - return this._textModel.getValue(); - } - - return this.cell.source.join('\n'); - } - - getHTML(): HTMLElement | null { - if (this.cellKind === CellKind.Markdown) { - if (this._html) { - return this._html; - } - let renderer = this.getMarkdownRenderer(); - this._html = renderer.render({ value: this.getText(), isTrusted: true }).element; - return this._html; - } - return null; - } - - async resolveTextModel(): Promise { - if (!this._textModel) { - const ref = await this._modelService.createModelReference(this.cell.uri); - this._textModel = ref.object.textEditorModel; - this._buffer = this._textModel.getTextBuffer(); - this._register(ref); - this._register(this._textModel.onDidChangeContent(() => { - this.cell.contentChange(); - this._html = null; - this._onDidChangeContent.fire(); - })); - } - return this._textModel; - } - - attachTextEditor(editor: ICodeEditor) { - if (!editor.hasModel()) { - throw new Error('Invalid editor: model is missing'); - } - - if (this._textEditor === editor) { - if (this._cursorChangeListener === null) { - this._cursorChangeListener = this._textEditor.onDidChangeCursorSelection(() => this._onDidChangeCursorSelection.fire()); - this._onDidChangeCursorSelection.fire(); - } - return; - } - - this._textEditor = editor; - - if (this._editorViewStates) { - this.restoreViewState(this._editorViewStates); - } - - this._resolvedDecorations.forEach((value, key) => { - if (key.startsWith('_lazy_')) { - // lazy ones - - const ret = this._textEditor!.deltaDecorations([], [value.options]); - this._resolvedDecorations.get(key)!.id = ret[0]; - } else { - const ret = this._textEditor!.deltaDecorations([], [value.options]); - this._resolvedDecorations.get(key)!.id = ret[0]; - } - }); - - this._cursorChangeListener = this._textEditor.onDidChangeCursorSelection(() => this._onDidChangeCursorSelection.fire()); - this._onDidChangeCursorSelection.fire(); - this._onDidChangeEditorAttachState.fire(true); - } - - detachTextEditor() { - this._editorViewStates = this.saveViewState(); - - // decorations need to be cleared first as editors can be resued. - this._resolvedDecorations.forEach(value => { - let resolvedid = value.id; - - if (resolvedid) { - this._textEditor?.deltaDecorations([resolvedid], []); - } - }); - this._textEditor = undefined; - this._cursorChangeListener?.dispose(); - this._cursorChangeListener = null; - this._onDidChangeEditorAttachState.fire(false); - } - - revealRangeInCenter(range: Range) { - this._textEditor?.revealRangeInCenter(range, editorCommon.ScrollType.Immediate); - } - - setSelection(range: Range) { - this._textEditor?.setSelection(range); - } - - getLineScrollTopOffset(line: number): number { - if (!this._textEditor) { - return 0; - } - - return this._textEditor.getTopForLineNumber(line) + EDITOR_TOP_PADDING + EDITOR_TOOLBAR_HEIGHT; - } - - addDecoration(decoration: model.IModelDeltaDecoration): string { - if (!this._textEditor) { - const id = ++this._lastDecorationId; - const decorationId = `_lazy_${this.id};${id}`; - - this._resolvedDecorations.set(decorationId, { options: decoration }); - return decorationId; - } - - const result = this._textEditor.deltaDecorations([], [decoration]); - this._resolvedDecorations.set(result[0], { id: result[0], options: decoration }); - - return result[0]; - } - - removeDecoration(decorationId: string) { - const realDecorationId = this._resolvedDecorations.get(decorationId); - - if (this._textEditor && realDecorationId && realDecorationId.id !== undefined) { - this._textEditor.deltaDecorations([realDecorationId.id!], []); - } - - // lastly, remove all the cache - this._resolvedDecorations.delete(decorationId); - } - - deltaDecorations(oldDecorations: string[], newDecorations: model.IModelDeltaDecoration[]): string[] { - oldDecorations.forEach(id => { - this.removeDecoration(id); - }); - - const ret = newDecorations.map(option => { - return this.addDecoration(option); - }); - - return ret; - } - - onDeselect() { - this.state = CellState.Preview; - } - - cursorAtBoundary(): CursorAtBoundary { - if (!this._textEditor) { - return CursorAtBoundary.None; - } - - // only validate primary cursor - const selection = this._textEditor.getSelection(); - - // only validate empty cursor - if (!selection || !selection.isEmpty()) { - return CursorAtBoundary.None; - } - - // we don't allow attaching text editor without a model - const lineCnt = this._textEditor.getModel()!.getLineCount(); - - if (selection.startLineNumber === lineCnt) { - // bottom - - if (selection.startLineNumber === 1) { - return CursorAtBoundary.Both; - } else { - return CursorAtBoundary.Bottom; - } - } - - if (selection.startLineNumber === 1) { - return CursorAtBoundary.Top; - } - - return CursorAtBoundary.None; - } - - getMarkdownRenderer() { - if (!this._mdRenderer) { - this._mdRenderer = this._instaService.createInstance(MarkdownRenderer); - } - return this._mdRenderer; - } - - updateOutputHeight(index: number, height: number) { - if (index >= this._outputCollection.length) { - throw new Error('Output index out of range!'); - } - - this._outputCollection[index] = height; - this._ensureOutputsTop(); - this._outputsTop!.changeValue(index, height); - this._onDidChangeTotalHeight.fire(); - } - - getOutputOffset(index: number): number { - if (index >= this._outputCollection.length) { - throw new Error('Output index out of range!'); - } - - this._ensureOutputsTop(); - - return this._outputsTop!.getAccumulatedValue(index - 1); - } - - getOutputHeight(output: IOutput): number | undefined { - let index = this.cell.outputs.indexOf(output); - - if (index < 0) { - return undefined; - } - - if (index < this._outputCollection.length) { - return this._outputCollection[index]; - } - - return undefined; - } - - private getOutputTotalHeight(): number { - this._ensureOutputsTop(); - - return this._outputsTop!.getTotalValue(); - } - - spliceOutputHeights(start: number, deleteCnt: number, heights: number[]) { - this._ensureOutputsTop(); - - this._outputsTop!.removeValues(start, deleteCnt); - if (heights.length) { - const values = new Uint32Array(heights.length); - for (let i = 0; i < heights.length; i++) { - values[i] = heights[i]; - } - - this._outputsTop!.insertValues(start, values); - } - - this._onDidChangeTotalHeight.fire(); - } - - getCellTotalHeight(): number { - if (this.outputs.length) { - return EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING + 16 + this.getOutputTotalHeight(); - } else { - return EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING + this.getOutputTotalHeight(); - } - } - - getIndicatorHeight(): number { - return this.getCellTotalHeight() - EDITOR_TOOLBAR_HEIGHT - 16; - } - - protected _ensureOutputsTop(): void { - if (!this._outputsTop) { - const values = new Uint32Array(this._outputCollection.length); - for (let i = 0; i < this._outputCollection.length; i++) { - values[i] = this._outputCollection[i]; - } - - this._outputsTop = new PrefixSumComputer(values); - } - } - - toJSON(): any { - return { - handle: this.handle - }; - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 047ee2ecc0..60e4a7856a 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -3,22 +3,24 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import * as editorCommon from 'vs/editor/common/editorCommon'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; -import { ICell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { CellFindMatch, CellState, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { URI } from 'vs/base/common/uri'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { Range } from 'vs/editor/common/core/range'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { WorkspaceTextEdit } from 'vs/editor/common/modes'; -import { URI } from 'vs/base/common/uri'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { InsertCellEdit, DeleteCellEdit, MoveCellEdit } from 'vs/workbench/contrib/notebook/browser/viewModel/cellEdit'; +import { CellFindMatch, CellEditState, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { DeleteCellEdit, InsertCellEdit, MoveCellEdit } from 'vs/workbench/contrib/notebook/browser/viewModel/cellEdit'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; +import { CellKind, ICell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEventDispatcher, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; export interface INotebookEditorViewState { editingCells: { [key: number]: boolean }; @@ -100,6 +102,7 @@ export class NotebookViewModel extends Disposable { constructor( public viewType: string, private _model: NotebookEditorModel, + readonly eventDispatcher: NotebookEventDispatcher, @IInstantiationService private readonly instantiationService: IInstantiationService, @IBulkEditService private readonly bulkEditService: IBulkEditService, @IUndoRedoService private readonly undoService: IUndoRedoService @@ -110,15 +113,19 @@ export class NotebookViewModel extends Disposable { this._onDidChangeViewCells.fire({ synchronous: true, splices: e.map(splice => { - return [splice[0], splice[1], splice[2].map(cell => this.instantiationService.createInstance(CellViewModel, this.viewType, this.handle, cell))]; + return [splice[0], splice[1], splice[2].map(cell => { + return createCellViewModel(this.instantiationService, this, cell); + })]; }) }); })); + this._register(this._model.notebook.onDidChangeMetadata(e => { + this.eventDispatcher.emit([new NotebookMetadataChangedEvent(e)]); + })); + this._viewCells = this._model!.notebook!.cells.map(cell => { - const viewCell = this.instantiationService.createInstance(CellViewModel, this.viewType, this._model!.notebook!.handle, cell); - this._localStore.add(viewCell); - return viewCell; + return createCellViewModel(this.instantiationService, this, cell); }); } @@ -129,7 +136,7 @@ export class NotebookViewModel extends Disposable { hide() { this._viewCells.forEach(cell => { if (cell.getText() !== '') { - cell.state = CellState.Preview; + cell.editState = CellEditState.Preview; } }); } @@ -152,7 +159,7 @@ export class NotebookViewModel extends Disposable { } insertCell(index: number, cell: ICell, synchronous: boolean): CellViewModel { - const newCell = this.instantiationService.createInstance(CellViewModel, this.viewType, this.handle, cell); + let newCell: CellViewModel = createCellViewModel(this.instantiationService, this, cell); this._viewCells!.splice(index, 0, newCell); this._model.insertCell(newCell.cell, index); this._localStore.add(newCell); @@ -172,8 +179,11 @@ export class NotebookViewModel extends Disposable { this.undoService.pushElement(new DeleteCellEdit(this.uri, index, viewCell, { insertCell: this._insertCellDelegate.bind(this), - deleteCell: this._deleteCellDelegate.bind(this) - }, this.instantiationService, this)); + deleteCell: this._deleteCellDelegate.bind(this), + createCellViewModel: (cell: ICell) => { + return createCellViewModel(this.instantiationService, this, cell); + } + })); this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] }); viewCell.dispose(); @@ -207,7 +217,7 @@ export class NotebookViewModel extends Disposable { saveEditorViewState(): INotebookEditorViewState { const state: { [key: number]: boolean } = {}; - this._viewCells.filter(cell => cell.state === CellState.Editing).forEach(cell => state[cell.cell.handle] = true); + this._viewCells.filter(cell => cell.editState === CellEditState.Editing).forEach(cell => state[cell.cell.handle] = true); const editorViewStates: { [key: number]: editorCommon.ICodeEditorViewState } = {}; this._viewCells.map(cell => ({ handle: cell.cell.handle, state: cell.saveEditorViewState() })).forEach(viewState => { if (viewState.state) { @@ -230,7 +240,7 @@ export class NotebookViewModel extends Disposable { const isEditing = viewState.editingCells && viewState.editingCells[cell.handle]; const editorViewState = viewState.editorViewStates && viewState.editorViewStates[cell.handle]; - cell.state = isEditing ? CellState.Editing : CellState.Preview; + cell.editState = isEditing ? CellEditState.Editing : CellEditState.Preview; cell.restoreEditorViewState(editorViewState); }); } @@ -382,3 +392,13 @@ export class NotebookViewModel extends Disposable { super.dispose(); } } + +export type CellViewModel = CodeCellViewModel | MarkdownCellViewModel; + +export function createCellViewModel(instantiationService: IInstantiationService, notebookViewModel: NotebookViewModel, cell: ICell) { + if (cell.cellKind === CellKind.Code) { + return instantiationService.createInstance(CodeCellViewModel, notebookViewModel.viewType, notebookViewModel.handle, cell, notebookViewModel.eventDispatcher); + } else { + return instantiationService.createInstance(MarkdownCellViewModel, notebookViewModel.viewType, notebookViewModel.handle, cell, notebookViewModel.eventDispatcher); + } +} diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 7fa2dde913..faab29962e 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { ICell, IOutput, NotebookCellOutputsSplice, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICell, IOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { PieceTreeTextBufferFactory, PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { URI } from 'vs/base/common/uri'; @@ -15,6 +15,9 @@ export class NotebookCellTextModel implements ICell { private _onDidChangeContent = new Emitter(); onDidChangeContent: Event = this._onDidChangeContent.event; + private _onDidChangeMetadata = new Emitter(); + onDidChangeMetadata: Event = this._onDidChangeMetadata.event; + private _outputs: IOutput[]; get outputs(): IOutput[] { @@ -30,6 +33,17 @@ export class NotebookCellTextModel implements ICell { this._buffer = null; } + private _metadata: NotebookCellMetadata | undefined; + + get metadata() { + return this._metadata; + } + + set metadata(newMetadata: NotebookCellMetadata | undefined) { + this._metadata = newMetadata; + this._onDidChangeMetadata.fire(this._metadata); + } + private _buffer: PieceTreeTextBufferFactory | null = null; constructor( @@ -38,9 +52,11 @@ export class NotebookCellTextModel implements ICell { private _source: string[], public language: string, public cellKind: CellKind, - outputs: IOutput[] + outputs: IOutput[], + metadata: NotebookCellMetadata | undefined ) { this._outputs = outputs; + this._metadata = metadata; } contentChange() { diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 83588fc9b5..27aa02eb44 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellsSplice, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellsSplice, NotebookDocumentMetadata, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class NotebookTextModel extends Disposable implements INotebookTextModel { private readonly _onWillDispose: Emitter = this._register(new Emitter()); @@ -16,12 +16,13 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel get onDidChangeCells(): Event { return this._onDidChangeCells.event; } private _onDidChangeContent = new Emitter(); onDidChangeContent: Event = this._onDidChangeContent.event; + private _onDidChangeMetadata = new Emitter(); + onDidChangeMetadata: Event = this._onDidChangeMetadata.event; private _mapping: Map = new Map(); private _cellListeners: Map = new Map(); cells: NotebookCellTextModel[]; - activeCell: NotebookCellTextModel | undefined; languages: string[] = []; - metadata: NotebookDocumentMetadata | undefined = undefined; + metadata: NotebookDocumentMetadata | undefined = { editable: true }; renderers = new Set(); constructor( @@ -37,8 +38,17 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this.languages = languages; } - updateNotebookMetadata(metadata: NotebookDocumentMetadata | undefined) { + updateNotebookMetadata(metadata: NotebookDocumentMetadata) { this.metadata = metadata; + this._onDidChangeMetadata.fire(this.metadata); + } + + updateNotebookCellMetadata(handle: number, metadata: NotebookCellMetadata) { + const cell = this.cells.find(cell => cell.handle === handle); + + if (cell) { + cell.metadata = metadata; + } } updateRenderers(renderers: number[]) { @@ -47,10 +57,6 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel }); } - updateActiveCell(handle: number) { - this.activeCell = this._mapping.get(handle); - } - insertNewCell(index: number, cell: NotebookCellTextModel): void { this._mapping.set(cell.handle, cell); this.cells.splice(index, 0, cell); @@ -77,7 +83,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel splices.reverse().forEach(splice => { let cellDtos = splice[2]; let newCells = cellDtos.map(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); this._mapping.set(cell.handle, mainCell); let dirtyStateListener = mainCell.onDidChangeContent(() => { this._onDidChangeContent.fire(); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 4f3a9ae74c..98ebf2cdf9 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -38,10 +38,13 @@ export const NOTEBOOK_DISPLAY_ORDER = [ export interface NotebookDocumentMetadata { editable: boolean; + cellEditable?: boolean; + cellRunnable?: boolean; } export interface NotebookCellMetadata { - editable: boolean; + editable?: boolean; + runnable?: boolean; } export interface INotebookDisplayOrder { @@ -132,6 +135,7 @@ export interface ICell { outputs: IOutput[]; metadata?: NotebookCellMetadata; onDidChangeOutputs?: Event; + onDidChangeMetadata: Event; resolveTextBufferFactory(): PieceTreeTextBufferFactory; // TODO@rebornix it should be later on replaced by moving textmodel resolution into CellTextModel contentChange(): void; diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index ff48e2bbfc..1dcc634dfa 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -13,6 +13,7 @@ import { withTestNotebook, TestCell } from 'vs/workbench/contrib/notebook/test/t import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; suite('NotebookViewModel', () => { const instantiationService = new TestInstantiationService(); @@ -23,7 +24,8 @@ suite('NotebookViewModel', () => { test('ctor', function () { const notebook = new NotebookTextModel(0, 'notebook', URI.parse('test')); const model = new NotebookEditorModel(notebook); - const viewModel = new NotebookViewModel('notebook', model, instantiationService, blukEditService, undoRedoService); + const eventDispatcher = new NotebookEventDispatcher(); + const viewModel = new NotebookViewModel('notebook', model, eventDispatcher, instantiationService, blukEditService, undoRedoService); assert.equal(viewModel.viewType, 'notebook'); }); @@ -33,10 +35,13 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, []], - [['var b = 2;'], 'javascript', CellKind.Code, []] + [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], + [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel) => { + assert.equal(viewModel.viewCells[0].metadata?.editable, true); + assert.equal(viewModel.viewCells[1].metadata?.editable, false); + const cell = viewModel.insertCell(1, new TestCell(viewModel.viewType, 0, ['var c = 3;'], 'javascript', CellKind.Code, []), true); assert.equal(viewModel.viewCells.length, 3); assert.equal(viewModel.notebookDocument.cells.length, 3); @@ -56,8 +61,8 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, []], - [['var b = 2;'], 'javascript', CellKind.Code, []] + [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], + [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true }] ], (editor, viewModel) => { const firstViewCell = viewModel.viewCells[0]; diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index d74879d05d..f5b062aa6a 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -6,12 +6,11 @@ import { Emitter, Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { PieceTreeTextBufferFactory } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; -import { CellKind, ICell, IOutput, NotebookCellOutputsSplice, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookViewModel, IModelDecorationsChangeAccessor } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellKind, ICell, IOutput, NotebookCellOutputsSplice, CellUri, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookViewModel, IModelDecorationsChangeAccessor, CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { INotebookEditor, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, NotebookLayoutInfo, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; @@ -20,13 +19,21 @@ import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; export class TestCell implements ICell { uri: URI; private _onDidChangeOutputs = new Emitter(); onDidChangeOutputs: Event = this._onDidChangeOutputs.event; + private _onDidChangeMetadata = new Emitter(); + onDidChangeMetadata: Event = this._onDidChangeMetadata.event; private _isDirty: boolean = false; private _outputs: IOutput[]; + + get metadata(): NotebookCellMetadata { + return { editable: true }; + } + get outputs(): IOutput[] { return this._outputs; } @@ -69,6 +76,10 @@ export class TestNotebookEditor implements INotebookEditor { constructor( ) { } + executeNotebookCell(cell: ICellViewModel): Promise { + throw new Error('Method not implemented.'); + } + isNotebookEditor = true; postMessage(message: any): void { @@ -179,20 +190,21 @@ export class TestNotebookEditor implements INotebookEditor { } } -export function createTestCellViewModel(instantiationService: IInstantiationService, viewType: string, notebookHandle: number, cellhandle: number, source: string[], language: string, cellKind: CellKind, outputs: IOutput[]) { - const mockCell = new TestCell(viewType, cellhandle, source, language, cellKind, outputs); - return instantiationService.createInstance(CellViewModel, viewType, notebookHandle, mockCell); -} +// export function createTestCellViewModel(instantiationService: IInstantiationService, viewType: string, notebookHandle: number, cellhandle: number, source: string[], language: string, cellKind: CellKind, outputs: IOutput[]) { +// const mockCell = new TestCell(viewType, cellhandle, source, language, cellKind, outputs); +// return createCellViewModel(instantiationService, viewType, notebookHandle, mockCell); +// } -export function withTestNotebook(instantiationService: IInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string[], string, CellKind, IOutput[]][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel) => void) { +export function withTestNotebook(instantiationService: IInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string[], string, CellKind, IOutput[], NotebookCellMetadata][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel) => void) { const viewType = 'notebook'; const editor = new TestNotebookEditor(); const notebook = new NotebookTextModel(0, viewType, URI.parse('test')); notebook.cells = cells.map((cell, index) => { - return new NotebookCellTextModel(notebook.uri, index, cell[0], cell[1], cell[2], cell[3]); + return new NotebookCellTextModel(notebook.uri, index, cell[0], cell[1], cell[2], cell[3], cell[4]); }); const model = new NotebookEditorModel(notebook); - const viewModel = new NotebookViewModel(viewType, model, instantiationService, blukEditService, undoRedoService); + const eventDispatcher = new NotebookEventDispatcher(); + const viewModel = new NotebookViewModel(viewType, model, eventDispatcher, instantiationService, blukEditService, undoRedoService); callback(editor, viewModel); diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 2cc128bae1..fa91baa538 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -32,7 +32,7 @@ import { KeybindingsEditor } from 'vs/workbench/contrib/preferences/browser/keyb import { ConfigureLanguageBasedSettingsAction, OpenDefaultKeybindingsFileAction, OpenFolderSettingsAction, OpenGlobalKeybindingsAction, OpenGlobalKeybindingsFileAction, OpenGlobalSettingsAction, OpenRawDefaultSettingsAction, OpenRemoteSettingsAction, OpenSettings2Action, OpenSettingsJsonAction, OpenWorkspaceSettingsAction, OPEN_FOLDER_SETTINGS_COMMAND, OPEN_FOLDER_SETTINGS_LABEL } from 'vs/workbench/contrib/preferences/browser/preferencesActions'; import { PreferencesEditor } from 'vs/workbench/contrib/preferences/browser/preferencesEditor'; import { SettingsEditor2 } from 'vs/workbench/contrib/preferences/browser/settingsEditor2'; -import { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, MODIFIED_SETTING_TAG, SETTINGS_COMMAND_OPEN_SETTINGS, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING, SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED, SETTINGS_EDITOR_COMMAND_FILTER_ONLINE, SETTINGS_EDITOR_COMMAND_FOCUS_FILE, SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH, SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_LIST, SETTINGS_EDITOR_COMMAND_SEARCH, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON } from 'vs/workbench/contrib/preferences/common/preferences'; +import { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, MODIFIED_SETTING_TAG, SETTINGS_COMMAND_OPEN_SETTINGS, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING, SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED, SETTINGS_EDITOR_COMMAND_FILTER_ONLINE, SETTINGS_EDITOR_COMMAND_FOCUS_FILE, SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH, SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_LIST, SETTINGS_EDITOR_COMMAND_SEARCH, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON, SETTINGS_EDITOR_COMMAND_FOCUS_TOC } from 'vs/workbench/contrib/preferences/common/preferences'; import { PreferencesContribution } from 'vs/workbench/contrib/preferences/common/preferencesContribution'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -765,6 +765,23 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_TOC, + precondition: CONTEXT_SETTINGS_EDITOR, + title: nls.localize('settings.focusSettingsTOC', "Focus settings TOC tree") + }); + } + + run(accessor: ServicesAccessor): void { + const preferencesEditor = getPreferencesEditor(accessor); + if (preferencesEditor instanceof SettingsEditor2) { + preferencesEditor.focusTOC(); + } + } +}); + registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 71856eb6e2..343330538c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -354,6 +354,10 @@ export class SettingsEditor2 extends BaseEditor { } } + focusTOC(): void { + this.tocTree.domFocus(); + } + showContextMenu(): void { const activeElement = this.getActiveElementInSettingsTree(); if (!activeElement) { diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index 6ad0304ca7..a2014a7bf1 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -83,6 +83,7 @@ export const SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING = 'settings.action.edi export const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH = 'settings.action.focusSettingsFromSearch'; export const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_LIST = 'settings.action.focusSettingsList'; export const SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU = 'settings.action.showContextMenu'; +export const SETTINGS_EDITOR_COMMAND_FOCUS_TOC = 'settings.action.focusTOC'; export const SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON = 'settings.switchToJSON'; export const SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED = 'settings.filterByModified'; diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 11f26d22fa..15d3057b74 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -20,6 +20,9 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { DefaultQuickAccessFilterValue } from 'vs/platform/quickinput/common/quickAccess'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchQuickOpenConfiguration } from 'vs/workbench/browser/quickopen'; export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAccessProvider { @@ -34,6 +37,14 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce protected get activeTextEditorControl(): IEditor | undefined { return this.editorService.activeTextEditorControl; } + get defaultFilterValue(): DefaultQuickAccessFilterValue | undefined { + if (this.configuration.preserveInput) { + return DefaultQuickAccessFilterValue.LAST; + } + + return undefined; + } + constructor( @IEditorService private readonly editorService: IEditorService, @IMenuService private readonly menuService: IMenuService, @@ -42,11 +53,20 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce @IKeybindingService keybindingService: IKeybindingService, @ICommandService commandService: ICommandService, @ITelemetryService telemetryService: ITelemetryService, - @INotificationService notificationService: INotificationService + @INotificationService notificationService: INotificationService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super({ showAlias: !Language.isDefaultVariant() }, instantiationService, keybindingService, commandService, telemetryService, notificationService); } + private get configuration() { + const commandPaletteConfig = this.configurationService.getValue().workbench.commandPalette; + + return { + preserveInput: commandPaletteConfig.preserveInput + }; + } + protected async getCommandPicks(disposables: DisposableStore, token: CancellationToken): Promise> { // wait for extensions registration or 800ms once diff --git a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts index 3f55e4a041..b28ac7ece5 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts @@ -8,10 +8,7 @@ import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/ import { Registry } from 'vs/platform/registry/common/platform'; import { HelpQuickAccessProvider } from 'vs/platform/quickinput/browser/helpQuickAccess'; import { ViewQuickAccessProvider } from 'vs/workbench/contrib/quickaccess/browser/viewQuickAccess'; -import { QUICK_ACCESS_COMMAND_ID, quickAccessCommand } from 'vs/workbench/contrib/quickaccess/browser/quickAccessCommands'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { CommandsQuickAccessProvider } from 'vs/workbench/contrib/quickaccess/browser/commandsQuickAccess'; -import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; const registry = Registry.as(Extensions.Quickaccess); @@ -25,6 +22,7 @@ registry.registerQuickAccessProvider({ registry.registerQuickAccessProvider({ ctor: ViewQuickAccessProvider, prefix: ViewQuickAccessProvider.PREFIX, + contextKey: 'inViewsPicker', placeholder: localize('viewQuickAccessPlaceholder', "Type the name of a view, output channel or terminal to open."), helpEntries: [{ description: localize('viewQuickAccess', "Open View"), needsEditor: false }] }); @@ -32,22 +30,7 @@ registry.registerQuickAccessProvider({ registry.registerQuickAccessProvider({ ctor: CommandsQuickAccessProvider, prefix: CommandsQuickAccessProvider.PREFIX, + contextKey: 'inCommandsPicker', placeholder: localize('commandsQuickAccessPlaceholder', "Type the name of a command to run."), helpEntries: [{ description: localize('commandsQuickAccess', "Show and Run Commands"), needsEditor: false }] }); - -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: QUICK_ACCESS_COMMAND_ID, title: { - value: localize('openQuickAccess', "Open Quick Access"), original: 'Open Quick Access' - }, - category: localize('quickAccess', "Quick Access") - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: QUICK_ACCESS_COMMAND_ID, - weight: KeybindingWeight.WorkbenchContrib, - when: undefined, - handler: quickAccessCommand.handler -}); diff --git a/src/vs/workbench/contrib/quickaccess/browser/quickAccessCommands.ts b/src/vs/workbench/contrib/quickaccess/browser/quickAccessCommands.ts deleted file mode 100644 index cc4e6a0a66..0000000000 --- a/src/vs/workbench/contrib/quickaccess/browser/quickAccessCommands.ts +++ /dev/null @@ -1,28 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { ICommand } from 'vs/platform/commands/common/commands'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; - -export const QUICK_ACCESS_COMMAND_ID = 'workbench.action.openQuickAccess'; - -export const quickAccessCommand: ICommand = { - id: QUICK_ACCESS_COMMAND_ID, - handler: async function (accessor: ServicesAccessor, prefix: string | null = null) { - const quickInputService = accessor.get(IQuickInputService); - - quickInputService.quickAccess.show(typeof prefix === 'string' ? prefix : undefined); - }, - description: { - description: `Quick access`, - args: [{ - name: 'prefix', - schema: { - 'type': 'string' - } - }] - } -}; diff --git a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts index e233c94d0f..abd04394da 100644 --- a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts @@ -12,7 +12,7 @@ import { Mode, IEntryRunContext, IAutoFocus, IModel, IQuickNavigateConfiguration import { QuickOpenEntryGroup, IHighlight, QuickOpenModel, QuickOpenEntry } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { QuickOpenHandler, IWorkbenchQuickOpenConfiguration } from 'vs/workbench/browser/quickopen'; +import { QuickOpenHandler, IWorkbenchQuickOpenConfiguration, ENABLE_EXPERIMENTAL_VERSION_CONFIG } from 'vs/workbench/browser/quickopen'; import { IEditorAction } from 'vs/editor/common/editorCommon'; import { matchesWords, matchesPrefix, matchesContiguousSubString, or } from 'vs/base/common/filters'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -34,6 +34,8 @@ import { Disposable, DisposableStore, IDisposable, toDisposable, dispose } from import { timeout } from 'vs/base/common/async'; import { isFirefox } from 'vs/base/browser/browser'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { CommandsHistory } from 'vs/platform/quickinput/browser/commandsQuickAccess'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; export const ALL_COMMANDS_PREFIX = '>'; @@ -42,7 +44,7 @@ interface ISerializedCommandHistory { entries: { key: string; value: number }[]; } -class CommandsHistory extends Disposable { +class LegacyCommandsHistory extends Disposable { static readonly DEFAULT_COMMANDS_HISTORY_LENGTH = 50; @@ -71,17 +73,17 @@ class CommandsHistory extends Disposable { } private updateConfiguration(): void { - this.configuredCommandsHistoryLength = CommandsHistory.getConfiguredCommandHistoryLength(this.configurationService); + this.configuredCommandsHistoryLength = LegacyCommandsHistory.getConfiguredCommandHistoryLength(this.configurationService); - if (CommandsHistory.cache && CommandsHistory.cache.limit !== this.configuredCommandsHistoryLength) { - CommandsHistory.cache.limit = this.configuredCommandsHistoryLength; + if (LegacyCommandsHistory.cache && LegacyCommandsHistory.cache.limit !== this.configuredCommandsHistoryLength) { + LegacyCommandsHistory.cache.limit = this.configuredCommandsHistoryLength; - CommandsHistory.saveState(this.storageService); + LegacyCommandsHistory.saveState(this.storageService); } } private load(): void { - const raw = this.storageService.get(CommandsHistory.PREF_KEY_CACHE, StorageScope.GLOBAL); + const raw = this.storageService.get(LegacyCommandsHistory.PREF_KEY_CACHE, StorageScope.GLOBAL); let serializedCache: ISerializedCommandHistory | undefined; if (raw) { try { @@ -91,7 +93,7 @@ class CommandsHistory extends Disposable { } } - const cache = CommandsHistory.cache = new LRUCache(this.configuredCommandsHistoryLength, 1); + const cache = LegacyCommandsHistory.cache = new LRUCache(this.configuredCommandsHistoryLength, 1); if (serializedCache) { let entries: { key: string; value: number }[]; if (serializedCache.usesLRU) { @@ -102,33 +104,33 @@ class CommandsHistory extends Disposable { entries.forEach(entry => cache.set(entry.key, entry.value)); } - CommandsHistory.counter = this.storageService.getNumber(CommandsHistory.PREF_KEY_COUNTER, StorageScope.GLOBAL, CommandsHistory.counter); + LegacyCommandsHistory.counter = this.storageService.getNumber(LegacyCommandsHistory.PREF_KEY_COUNTER, StorageScope.GLOBAL, LegacyCommandsHistory.counter); } push(commandId: string): void { - if (!CommandsHistory.cache) { + if (!LegacyCommandsHistory.cache) { return; } - CommandsHistory.cache.set(commandId, CommandsHistory.counter++); // set counter to command + LegacyCommandsHistory.cache.set(commandId, LegacyCommandsHistory.counter++); // set counter to command - CommandsHistory.saveState(this.storageService); + LegacyCommandsHistory.saveState(this.storageService); } peek(commandId: string): number | undefined { - return CommandsHistory.cache?.peek(commandId); + return LegacyCommandsHistory.cache?.peek(commandId); } static saveState(storageService: IStorageService): void { - if (!CommandsHistory.cache) { + if (!LegacyCommandsHistory.cache) { return; } const serializedCache: ISerializedCommandHistory = { usesLRU: true, entries: [] }; - CommandsHistory.cache.forEach((value, key) => serializedCache.entries.push({ key, value })); + LegacyCommandsHistory.cache.forEach((value, key) => serializedCache.entries.push({ key, value })); - storageService.store(CommandsHistory.PREF_KEY_CACHE, JSON.stringify(serializedCache), StorageScope.GLOBAL); - storageService.store(CommandsHistory.PREF_KEY_COUNTER, CommandsHistory.counter, StorageScope.GLOBAL); + storageService.store(LegacyCommandsHistory.PREF_KEY_CACHE, JSON.stringify(serializedCache), StorageScope.GLOBAL); + storageService.store(LegacyCommandsHistory.PREF_KEY_COUNTER, LegacyCommandsHistory.counter, StorageScope.GLOBAL); } static getConfiguredCommandHistoryLength(configurationService: IConfigurationService): number { @@ -139,15 +141,15 @@ class CommandsHistory extends Disposable { return configuredCommandHistoryLength; } - return CommandsHistory.DEFAULT_COMMANDS_HISTORY_LENGTH; + return LegacyCommandsHistory.DEFAULT_COMMANDS_HISTORY_LENGTH; } static clearHistory(configurationService: IConfigurationService, storageService: IStorageService): void { - const commandHistoryLength = CommandsHistory.getConfiguredCommandHistoryLength(configurationService); - CommandsHistory.cache = new LRUCache(commandHistoryLength); - CommandsHistory.counter = 1; + const commandHistoryLength = LegacyCommandsHistory.getConfiguredCommandHistoryLength(configurationService); + LegacyCommandsHistory.cache = new LRUCache(commandHistoryLength); + LegacyCommandsHistory.counter = 1; - CommandsHistory.saveState(storageService); + LegacyCommandsHistory.saveState(storageService); } } @@ -162,6 +164,7 @@ export class ShowAllCommandsAction extends Action { id: string, label: string, @IQuickOpenService private readonly quickOpenService: IQuickOpenService, + @IQuickInputService private readonly quickInputService: IQuickInputService, @IConfigurationService private readonly configurationService: IConfigurationService ) { super(id, label); @@ -171,13 +174,18 @@ export class ShowAllCommandsAction extends Action { const config = this.configurationService.getValue(); const restoreInput = config.workbench?.commandPalette?.preserveInput === true; - // Show with last command palette input if any and configured - let value = ALL_COMMANDS_PREFIX; - if (restoreInput && lastCommandPaletteInput) { - value = `${value}${lastCommandPaletteInput}`; - } + if (this.configurationService.getValue(ENABLE_EXPERIMENTAL_VERSION_CONFIG) === true) { + this.quickInputService.quickAccess.show(ALL_COMMANDS_PREFIX); + } else { - this.quickOpenService.show(value, { inputSelection: lastCommandPaletteInput ? { start: 1 /* after prefix */, end: value.length } : undefined }); + // Show with last command palette input if any and configured + let value = ALL_COMMANDS_PREFIX; + if (restoreInput && lastCommandPaletteInput) { + value = `${value}${lastCommandPaletteInput}`; + } + + this.quickOpenService.show(value, { inputSelection: lastCommandPaletteInput ? { start: 1 /* after prefix */, end: value.length } : undefined }); + } return Promise.resolve(undefined); } @@ -197,13 +205,16 @@ export class ClearCommandHistoryAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { + const legacyCommandHistoryLength = LegacyCommandsHistory.getConfiguredCommandHistoryLength(this.configurationService); + if (legacyCommandHistoryLength > 0) { + LegacyCommandsHistory.clearHistory(this.configurationService, this.storageService); + } + const commandHistoryLength = CommandsHistory.getConfiguredCommandHistoryLength(this.configurationService); if (commandHistoryLength > 0) { CommandsHistory.clearHistory(this.configurationService, this.storageService); } - - return Promise.resolve(undefined); } } @@ -222,13 +233,11 @@ class CommandPaletteEditorAction extends EditorAction { }); } - run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { const quickOpenService = accessor.get(IQuickOpenService); // Show with prefix quickOpenService.show(ALL_COMMANDS_PREFIX); - - return Promise.resolve(undefined); } } @@ -406,7 +415,7 @@ export class CommandsHandler extends QuickOpenHandler implements IDisposable { static readonly ID = 'workbench.picker.commands'; private commandHistoryEnabled: boolean | undefined; - private readonly commandsHistory: CommandsHistory; + private readonly commandsHistory: LegacyCommandsHistory; private readonly disposables = new DisposableStore(); private readonly disposeOnClose = new DisposableStore(); @@ -423,7 +432,7 @@ export class CommandsHandler extends QuickOpenHandler implements IDisposable { ) { super(); - this.commandsHistory = this.disposables.add(this.instantiationService.createInstance(CommandsHistory)); + this.commandsHistory = this.disposables.add(this.instantiationService.createInstance(LegacyCommandsHistory)); this.extensionService.whenInstalledExtensionsRegistered().then(() => this.waitedForExtensionsRegistered = true); @@ -432,7 +441,7 @@ export class CommandsHandler extends QuickOpenHandler implements IDisposable { } private updateConfiguration(): void { - this.commandHistoryEnabled = CommandsHistory.getConfiguredCommandHistoryLength(this.configurationService) > 0; + this.commandHistoryEnabled = LegacyCommandsHistory.getConfiguredCommandHistoryLength(this.configurationService) > 0; } async getResults(searchValue: string, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/quickopen/browser/gotoLineHandler.ts b/src/vs/workbench/contrib/quickopen/browser/gotoLineHandler.ts index 199a3133cd..c28be3de12 100644 --- a/src/vs/workbench/contrib/quickopen/browser/gotoLineHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/gotoLineHandler.ts @@ -22,31 +22,29 @@ import { isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Event } from 'vs/base/common/event'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; export const GOTO_LINE_PREFIX = ':'; export class GotoLineAction extends QuickOpenAction { static readonly ID = 'workbench.action.gotoLine'; - static readonly LABEL = nls.localize('gotoLine', "Go to Line..."); + static readonly LABEL = nls.localize('gotoLine', "Go to Line/Column..."); constructor(actionId: string, actionLabel: string, - @IQuickOpenService private readonly _quickOpenService: IQuickOpenService, + @IQuickOpenService quickOpenService: IQuickOpenService, + @IQuickInputService private readonly quickInputService: IQuickInputService, @IEditorService private readonly editorService: IEditorService ) { - super(actionId, actionLabel, GOTO_LINE_PREFIX, _quickOpenService); + super(actionId, actionLabel, GOTO_LINE_PREFIX, quickOpenService); } - run(): Promise { - + async run(): Promise { let activeTextEditorControl = this.editorService.activeTextEditorControl; - if (!activeTextEditorControl) { - return Promise.resolve(); - } - if (isDiffEditor(activeTextEditorControl)) { activeTextEditorControl = activeTextEditorControl.getModifiedEditor(); } + let restoreOptions: IEditorOptions | null = null; if (isCodeEditor(activeTextEditorControl)) { @@ -65,7 +63,7 @@ export class GotoLineAction extends QuickOpenAction { const result = super.run(); if (restoreOptions) { - Event.once(this._quickOpenService.onHide)(() => { + Event.once(Event.any(this.quickOpenService.onHide, this.quickInputService.onHide))(() => { activeTextEditorControl!.updateOptions(restoreOptions!); }); } diff --git a/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts b/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts index 243590beb9..07696610c8 100644 --- a/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts @@ -61,7 +61,7 @@ const NLS_SYMBOL_KIND_CACHE: { [type: number]: string } = { export class GotoSymbolAction extends QuickOpenAction { static readonly ID = 'workbench.action.gotoSymbol'; - static readonly LABEL = nls.localize('gotoSymbol', "Go to Symbol in File..."); + static readonly LABEL = nls.localize('gotoSymbol', "Go to Symbol in Editor..."); constructor(actionId: string, actionLabel: string, @IQuickOpenService quickOpenService: IQuickOpenService) { super(actionId, actionLabel, GOTO_SYMBOL_PREFIX, quickOpenService); diff --git a/src/vs/workbench/contrib/quickopen/browser/quickopen.contribution.ts b/src/vs/workbench/contrib/quickopen/browser/quickopen.contribution.ts index 7cc96014e0..a71f211f63 100644 --- a/src/vs/workbench/contrib/quickopen/browser/quickopen.contribution.ts +++ b/src/vs/workbench/contrib/quickopen/browser/quickopen.contribution.ts @@ -30,11 +30,11 @@ registry.registerWorkbenchAction(SyncActionDescriptor.create(ShowAllCommandsActi registry.registerWorkbenchAction(SyncActionDescriptor.create(GotoLineAction, GotoLineAction.ID, GotoLineAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_G } -}), 'Go to Line...'); +}), 'Go to Line/Column...'); registry.registerWorkbenchAction(SyncActionDescriptor.create(GotoSymbolAction, GotoSymbolAction.ID, GotoSymbolAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O -}), 'Go to Symbol in File...'); +}), 'Go to Symbol in Editor...'); const inViewsPickerContextKey = 'inViewsPicker'; const inViewsPickerContext = ContextKeyExpr.and(inQuickOpenContext, ContextKeyExpr.has(inViewsPickerContextKey)); @@ -91,7 +91,7 @@ Registry.as(QuickOpenExtensions.Quickopen).registerQuickOpen { prefix: GOTO_LINE_PREFIX, needsEditor: true, - description: env.isMacintosh ? nls.localize('gotoLineDescriptionMac', "Go to Line") : nls.localize('gotoLineDescriptionWin', "Go to Line") + description: env.isMacintosh ? nls.localize('gotoLineDescriptionMac', "Go to Line/Column") : nls.localize('gotoLineDescriptionWin', "Go to Line/Column") }, ] ) @@ -107,12 +107,12 @@ Registry.as(QuickOpenExtensions.Quickopen).registerQuickOpen { prefix: GOTO_SYMBOL_PREFIX, needsEditor: true, - description: nls.localize('gotoSymbolDescription', "Go to Symbol in File") + description: nls.localize('gotoSymbolDescription', "Go to Symbol in Editor") }, { prefix: GOTO_SYMBOL_PREFIX + SCOPE_PREFIX, needsEditor: true, - description: nls.localize('gotoSymbolDescriptionScoped', "Go to Symbol in File by Category") + description: nls.localize('gotoSymbolDescriptionScoped', "Go to Symbol in Editor by Category") } ] ) @@ -170,7 +170,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { group: '4_symbol_nav', command: { id: 'workbench.action.gotoSymbol', - title: nls.localize({ key: 'miGotoSymbolInFile', comment: ['&& denotes a mnemonic'] }, "Go to &&Symbol in File...") + title: nls.localize({ key: 'miGotoSymbolInEditor', comment: ['&& denotes a mnemonic'] }, "Go to &&Symbol in Editor...") }, order: 1 }); diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 44cddca169..5f7fa857fb 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -15,11 +15,11 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { untildify } from 'vs/base/common/labels'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { URI } from 'vs/base/common/uri'; -import { toLocalResource, dirname, basenameOrAuthority } from 'vs/base/common/resources'; +import { toLocalResource, dirname, basenameOrAuthority, isEqual } from 'vs/base/common/resources'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileService } from 'vs/platform/files/common/files'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable, MutableDisposable, Disposable } from 'vs/base/common/lifecycle'; import { ILabelService } from 'vs/platform/label/common/label'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -39,11 +39,31 @@ import { Schemas } from 'vs/base/common/network'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ResourceMap } from 'vs/base/common/map'; import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; +import { DefaultQuickAccessFilterValue } from 'vs/platform/quickinput/common/quickAccess'; +import { IWorkbenchQuickOpenConfiguration } from 'vs/workbench/browser/quickopen'; +import { GotoSymbolQuickAccessProvider } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ScrollType, IEditor, ICodeEditorViewState, IDiffEditorViewState } from 'vs/editor/common/editorCommon'; +import { once } from 'vs/base/common/functional'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { withNullAsUndefined } from 'vs/base/common/types'; interface IAnythingQuickPickItem extends IPickerQuickAccessItem { resource: URI | undefined; } +interface IEditorSymbolAnythingQuickPickItem extends IAnythingQuickPickItem { + resource: URI; + range: { decoration: IRange, selection: IRange } +} + +function isEditorSymbolQuickPickItem(pick?: IAnythingQuickPickItem): pick is IEditorSymbolAnythingQuickPickItem { + const candidate = pick ? pick as IEditorSymbolAnythingQuickPickItem : undefined; + + return !!candidate && !!candidate.range && !!candidate.resource; +} + export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { static PREFIX = ''; @@ -53,27 +73,76 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider | undefined = undefined; + + editorViewState: { + editor: IEditorInput, + group: IEditorGroup, + state: ICodeEditorViewState | IDiffEditorViewState | undefined + } | undefined = undefined; + scorerCache: ScorerCache = Object.create(null); fileQueryCache: FileQueryCacheState | undefined = undefined; lastOriginalFilter: string | undefined = undefined; lastFilter: string | undefined = undefined; lastRange: IRange | undefined = undefined; + lastActiveGlobalPick: IAnythingQuickPickItem | undefined = undefined; - constructor(private readonly provider: AnythingQuickAccessProvider) { } + isQuickNavigating: boolean | undefined = undefined; - reset(): void { + constructor(private readonly provider: AnythingQuickAccessProvider, private readonly editorService: IEditorService) { } + + set(picker: IQuickPick): void { + + // Picker for this run + this.picker = picker; + once(picker.onDispose)(() => { + if (picker === this.picker) { + this.picker = undefined; // clear the picker when disposed to not keep it in memory for too long + } + }); // Caches - this.fileQueryCache = this.provider.createFileQueryCache(); - this.scorerCache = Object.create(null); + const isQuickNavigating = !!picker.quickNavigate; + if (!isQuickNavigating) { + this.fileQueryCache = this.provider.createFileQueryCache(); + this.scorerCache = Object.create(null); + } // Other + this.isQuickNavigating = isQuickNavigating; this.lastOriginalFilter = undefined; this.lastFilter = undefined; this.lastRange = undefined; + this.lastActiveGlobalPick = undefined; + this.editorViewState = undefined; } - }(this); + + rememberEditorViewState(): void { + if (this.editorViewState) { + return; // return early if already done + } + + const activeEditorPane = this.editorService.activeEditorPane; + if (activeEditorPane) { + this.editorViewState = { + group: activeEditorPane.group, + editor: activeEditorPane.input, + state: withNullAsUndefined(getCodeEditor(activeEditorPane.getControl())?.saveViewState()) + }; + } + } + }(this, this.editorService); + + get defaultFilterValue(): DefaultQuickAccessFilterValue | undefined { + if (this.configuration.preserveInput) { + return DefaultQuickAccessFilterValue.LAST; + } + + return undefined; + } constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -89,38 +158,93 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider().workbench.editor; - const searchConfig = this.configurationService.getValue(); + const searchConfig = this.configurationService.getValue().search; + const quickOpenConfig = this.configurationService.getValue().workbench.quickOpen; return { openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, openSideBySideDirection: editorConfig.openSideBySideDirection, - includeSymbols: searchConfig.search.quickOpen.includeSymbols, - workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter, - includeHistory: searchConfig.search.quickOpen.includeHistory, - shortAutoSaveDelay: this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY + includeSymbols: searchConfig.quickOpen.includeSymbols, + includeHistory: searchConfig.quickOpen.includeHistory, + historyFilterSortOrder: searchConfig.quickOpen.history.filterSortOrder, + shortAutoSaveDelay: this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY, + preserveInput: quickOpenConfig.preserveInput }; } provide(picker: IQuickPick, token: CancellationToken): IDisposable { + const disposables = new DisposableStore(); - // Reset the pick state for this run - this.pickState.reset(); + // Update the pick state for this run + this.pickState.set(picker); + + // Add editor decorations for active editor symbol picks + const editorDecorationsDisposable = disposables.add(new MutableDisposable()); + disposables.add(picker.onDidChangeActive(() => { + + // Clear old decorations + editorDecorationsDisposable.value = undefined; + + // Add new decoration if editor symbol is active + const [item] = picker.activeItems; + if (isEditorSymbolQuickPickItem(item)) { + editorDecorationsDisposable.value = this.decorateAndRevealSymbolRange(item); + } + })); + + // Restore view state upon cancellation if we changed it + disposables.add(once(token.onCancellationRequested)(() => { + if (this.pickState.editorViewState) { + this.editorService.openEditor( + this.pickState.editorViewState.editor, + { viewState: this.pickState.editorViewState.state, preserveFocus: true /* import to not close the picker as a result */ }, + this.pickState.editorViewState.group + ); + } + })); // Start picker - return super.provide(picker, token); + disposables.add(super.provide(picker, token)); + + return disposables; } - protected getPicks(originalFilter: string, disposables: DisposableStore, token: CancellationToken): FastAndSlowPicksType | null { + private decorateAndRevealSymbolRange(pick: IEditorSymbolAnythingQuickPickItem): IDisposable { + const activeEditor = this.editorService.activeEditor; + if (!isEqual(pick.resource, activeEditor?.resource)) { + return Disposable.None; // active editor needs to be for resource + } + + const activeEditorControl = this.editorService.activeTextEditorControl; + if (!activeEditorControl) { + return Disposable.None; // we need a text editor control to decorate and reveal + } + + // we must remember our curret view state to be able to restore + this.pickState.rememberEditorViewState(); + + // Reveal + activeEditorControl.revealRangeInCenter(pick.range.selection, ScrollType.Smooth); + + // Decorate + this.addDecorations(activeEditorControl, pick.range.decoration); + + return toDisposable(() => this.clearDecorations(activeEditorControl)); + } + + protected getPicks(originalFilter: string, disposables: DisposableStore, token: CancellationToken): Promise> | FastAndSlowPicksType | null { // Find a suitable range from the pattern looking for ":", "#" or "," - const filterWithRange = extractRangeFromFilter(originalFilter); + // unless we have the `@` editor symbol character inside the filter + const filterWithRange = extractRangeFromFilter(originalFilter, [GotoSymbolQuickAccessProvider.PREFIX]); // Update filter with normalized values let filter: string; @@ -145,18 +269,39 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider> | FastAndSlowPicksType | null { const query = prepareQuery(filter); + // Return early if we have editor symbol picks. We support this by: + // - having a previously active global pick (e.g. a file) + // - the user typing `@` to start the local symbol query + const editorSymbolPicks = this.getEditorSymbolPicks(query, disposables, token); + if (editorSymbolPicks) { + return editorSymbolPicks; + } + + // Otherwise return normally with history and file/symbol results const historyEditorPicks = this.getEditorHistoryPicks(query); return { // Fast picks: editor history - picks: historyEditorPicks.length > 0 ? - [ - { type: 'separator', label: localize('recentlyOpenedSeparator', "recently opened") }, - ...historyEditorPicks - ] : [], + picks: + (this.pickState.isQuickNavigating || historyEditorPicks.length === 0) ? + historyEditorPicks : + [ + { type: 'separator', label: localize('recentlyOpenedSeparator', "recently opened") }, + ...historyEditorPicks + ], // Slow picks: files and symbols additionalPicks: (async (): Promise> => { @@ -187,7 +332,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + private getEditorHistoryPicks(query: IPreparedQuery): Array { const configuration = this.configuration; // Just return all history entries if not searching @@ -261,7 +406,12 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider compareItemsByScore(editorA, editorB, query, false, editorHistoryScorerAccessor, this.pickState.scorerCache, () => -1)); + // Return without sorting if settings tell to sort by recency + if (this.configuration.historyFilterSortOrder === 'recency') { + return editorHistoryPicks; + } + + return editorHistoryPicks.sort((editorA, editorB) => compareItemsByScore(editorA, editorB, query, false, editorHistoryScorerAccessor, this.pickState.scorerCache)); } //#endregion @@ -282,7 +432,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider, token: CancellationToken): Promise> { + private async getFilePicks(query: IPreparedQuery, excludes: ResourceMap, token: CancellationToken): Promise> { if (!query.value) { return []; } @@ -346,9 +496,21 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider result.resource); + } + + // Otherwise, make sure to filter relative path results from + // the search results to prevent duplicates + const relativePathFileResultsMap = new ResourceMap(); + for (const relativePathFileResult of relativePathFileResults) { + relativePathFileResultsMap.set(relativePathFileResult, true); + } + return [ - ...fileSearchResults.results.map(result => result.resource), - ...(relativePathFileResults || []) + ...fileSearchResults.results.filter(result => !relativePathFileResultsMap.has(result.resource)).map(result => result.resource), + ...relativePathFileResults ]; } @@ -440,11 +602,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider> { + private async getWorkspaceSymbolPicks(query: IPreparedQuery, token: CancellationToken): Promise> { const configuration = this.configuration; if ( !query.value || // we need a value for search for @@ -456,8 +618,8 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider> | null { + const filter = query.original.split(GotoSymbolQuickAccessProvider.PREFIX)[1]?.trim(); + if (typeof filter !== 'string') { + return null; // we need to be searched for editor symbols via `@` + } + + const activeGlobalPick = this.pickState.lastActiveGlobalPick; + if (!activeGlobalPick) { + return null; // we need an active global pick to find symbols for + } + + const activeGlobalResource = activeGlobalPick.resource; + if (!activeGlobalResource || (!this.fileService.canHandleResource(activeGlobalResource) && activeGlobalResource.scheme !== Schemas.untitled)) { + return null; // we need a resource that we can resolve + } + + return this.doGetEditorSymbolPicks(activeGlobalPick, activeGlobalResource, filter, disposables, token); + } + + private async doGetEditorSymbolPicks(activeGlobalPick: IAnythingQuickPickItem, activeGlobalResource: URI, filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { + + // Bring the editor to front to review symbols to go to + try { + + // we must remember our curret view state to be able to restore + this.pickState.rememberEditorViewState(); + + // open it + await this.editorService.openEditor({ + resource: activeGlobalResource, + options: { preserveFocus: true, revealIfOpened: true, ignoreError: true } + }); + } catch (error) { + return []; // return if resource cannot be opened + } + + if (token.isCancellationRequested) { + return []; + } + + // Obtain model from resource + let model = this.modelService.getModel(activeGlobalResource); + if (!model) { + try { + const modelReference = disposables.add(await this.textModelService.createModelReference(activeGlobalResource)); + if (token.isCancellationRequested) { + return []; + } + + model = modelReference.object.textEditorModel; + } catch (error) { + return []; // return if model cannot be resolved + } + } + + // Ask provider for editor symbols + const editorSymbolPicks = (await this.editorSymbolsQuickAccess.getSymbolPicks(model, filter, disposables, token)); + if (token.isCancellationRequested) { + return []; + } + + return editorSymbolPicks.map(editorSymbolPick => { + + // Preserve separators + if (editorSymbolPick.type === 'separator') { + return editorSymbolPick; + } + + // Convert editor symbols to anything pick + return { + ...editorSymbolPick, + resource: activeGlobalResource, + description: editorSymbolPick.description ? `${activeGlobalPick.label} • ${editorSymbolPick.description}` : activeGlobalPick.label, + trigger: (buttonIndex, keyMods) => { + this.openAnything(activeGlobalResource, { keyMods, range: editorSymbolPick.range?.selection, forceOpenSideBySide: true }); + + return TriggerAction.CLOSE_PICKER; + }, + accept: (keyMods, event) => this.openAnything(activeGlobalResource, { keyMods, range: editorSymbolPick.range?.selection, preserveFocus: event.inBackground }) + }; + }); + } + + addDecorations(editor: IEditor, range: IRange): void { + this.editorSymbolsQuickAccess.addDecorations(editor, range); + } + + clearDecorations(editor: IEditor): void { + this.editorSymbolsQuickAccess.clearDecorations(editor); + } + + //#endregion + + //#region Helpers private createAnythingPick(resourceOrEditor: URI | IEditorInput | IResourceEditorInput, configuration: { shortAutoSaveDelay: boolean, openSideBySideDirection: 'right' | 'down' | undefined }): IAnythingQuickPickItem { @@ -497,6 +757,10 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + if (this.pickState.isQuickNavigating) { + return undefined; // no actions when quick navigating + } + const openSideBySideDirection = configuration.openSideBySideDirection; const buttons: IQuickInputButton[] = []; @@ -517,12 +781,13 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + trigger: (buttonIndex, keyMods) => { switch (buttonIndex) { // Open to side / below case 0: this.openAnything(resourceOrEditor, { keyMods, range: this.pickState.lastRange, forceOpenSideBySide: true }); + return TriggerAction.CLOSE_PICKER; // Remove from History @@ -530,7 +795,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + async run(): Promise { + if (this.configurationService.getValue(ENABLE_EXPERIMENTAL_VERSION_CONFIG) === true) { + this.quickInputService.quickAccess.show(ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX); + } else { + let prefix = ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX; + let inputSelection: { start: number; end: number; } | undefined = undefined; + const editor = this.editorService.getFocusedCodeEditor(); + const word = editor && getSelectionSearchString(editor); + if (word) { + prefix = prefix + word; + inputSelection = { start: 1, end: word.length + 1 }; + } - let prefix = ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX; - let inputSelection: { start: number; end: number; } | undefined = undefined; - const editor = this.editorService.getFocusedCodeEditor(); - const word = editor && getSelectionSearchString(editor); - if (word) { - prefix = prefix + word; - inputSelection = { start: 1, end: word.length + 1 }; + this.quickOpenService.show(prefix, { inputSelection }); } - - this.quickOpenService.show(prefix, { inputSelection }); - - return Promise.resolve(undefined); } } @@ -660,8 +668,8 @@ const quickAccessRegistry = Registry.as(QuickAccessExtensi quickAccessRegistry.registerQuickAccessProvider({ ctor: AnythingQuickAccessProvider, prefix: AnythingQuickAccessProvider.PREFIX, - placeholder: nls.localize('anythingQuickAccessPlaceholder', "Type '?' to get help on the actions you can take from here"), - contextKey: 'inFilesPicker', + placeholder: nls.localize('anythingQuickAccessPlaceholder', "Search files by name (append {0} to go to line or {1} to go to symbol)", AbstractGotoLineQuickAccessProvider.PREFIX, GotoSymbolQuickAccessProvider.PREFIX), + contextKey: defaultQuickOpenContextKey, helpEntries: [{ description: nls.localize('anythingQuickAccess', "Go to File"), needsEditor: false }] }); @@ -734,22 +742,21 @@ configurationRegistry.registerConfiguration({ description: nls.localize('search.quickOpen.includeSymbols', "Whether to include results from a global symbol search in the file results for Quick Open."), default: false }, - 'search.quickOpen.workspaceSymbolsFilter': { - type: 'string', - enum: ['default', 'reduced', 'all'], - markdownEnumDescriptions: [ - nls.localize('search.quickOpen.workspaceSymbolsFilter.default', "All symbols including local variables are included in the specific workspace symbols picker but excluded from the files picker when `#search.quickOpen.includeSymbols#` is enabled."), - nls.localize('search.quickOpen.workspaceSymbolsFilter.reduced', "Some symbols like local variables are excluded in all pickers."), - nls.localize('search.quickOpen.workspaceSymbolsFilter.all', "All symbols including local variables are included in all pickers.") - ], - default: 'default', - description: nls.localize('search.quickOpen.workspaceSymbolsFilter', "Controls the filter to apply for the workspace symbols search in quick open. Depending on the setting, some symbols like local variables will be excluded to reduce the total number of results."), - }, 'search.quickOpen.includeHistory': { type: 'boolean', description: nls.localize('search.quickOpen.includeHistory', "Whether to include results from recently opened files in the file results for Quick Open."), default: true }, + 'search.quickOpen.history.filterSortOrder': { + 'type': 'string', + 'enum': ['default', 'recency'], + 'default': 'default', + 'enumDescriptions': [ + nls.localize('filterSortOrder.default', 'History entries are sorted by relevance based on the filter value used. More relevant entries appear first.'), + nls.localize('filterSortOrder.recency', 'History entries are sorted by recency. More recently opened entries appear first.') + ], + 'description': nls.localize('filterSortOrder', "Controls sorting order of editor history in quick open when filtering.") + }, 'search.followSymlinks': { type: 'boolean', description: nls.localize('search.followSymlinks', "Controls whether to follow symlinks while searching."), diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index 16cb5f0302..7d02973981 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -145,7 +145,7 @@ export abstract class FindOrReplaceInFilesAction extends Action { const searchAndReplaceWidget = openedView.searchAndReplaceWidget; searchAndReplaceWidget.toggleReplace(this.expandSearchReplaceWidget); - const updatedText = openedView.updateTextFromSelection(!this.expandSearchReplaceWidget); + const updatedText = openedView.updateTextFromSelection({ allowUnselectedWord: !this.expandSearchReplaceWidget }); openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); } }); @@ -172,7 +172,7 @@ export const FindInFilesCommand: ICommandHandler = (accessor, args: IFindInFiles if (typeof args.query === 'string') { openedView.setSearchParameters(args); } else { - updatedText = openedView.updateTextFromSelection((typeof args.replace !== 'string')); + updatedText = openedView.updateTextFromSelection({ allowUnselectedWord: typeof args.replace !== 'string' }); } openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); } diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 64ac6ad633..b0d40dad4e 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -867,11 +867,11 @@ export class SearchView extends ViewPane { focus(): void { super.focus(); - const updatedText = this.updateTextFromSelection(); + const updatedText = this.updateTextFromSelection({ allowSearchOnType: false }); this.searchWidget.focus(undefined, undefined, updatedText); } - updateTextFromSelection(allowUnselectedWord = true): boolean { + updateTextFromSelection({ allowUnselectedWord = true, allowSearchOnType = true }): boolean { let updatedText = false; const seedSearchStringFromSelection = this.configurationService.getValue('editor').find!.seedSearchStringFromSelection; if (seedSearchStringFromSelection) { @@ -880,9 +880,14 @@ export class SearchView extends ViewPane { if (this.searchWidget.searchInput.getRegex()) { selectedText = strings.escapeRegExpCharacters(selectedText); } - this.pauseSearching = true; - this.searchWidget.setValue(selectedText); - this.pauseSearching = false; + + if (allowSearchOnType && !this.viewModel.searchResult.hasRemovedResults) { + this.searchWidget.setValue(selectedText); + } else { + this.pauseSearching = true; + this.searchWidget.setValue(selectedText); + this.pauseSearching = false; + } updatedText = true; } } @@ -1891,9 +1896,11 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = collector.addRule(`.monaco-workbench .search-view .monaco-list.element-focused .monaco-list-row.focused.selected:not(.highlighted) .action-label:focus { outline-color: ${outlineSelectionColor} }`); } - const foregroundColor = theme.getColor(foreground); - if (foregroundColor) { - const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, 0.65)); - collector.addRule(`.vs-dark .search-view .message { color: ${fgWithOpacity}; }`); + if (theme.type === 'dark') { + const foregroundColor = theme.getColor(foreground); + if (foregroundColor) { + const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, 0.65)); + collector.addRule(`.search-view .message { color: ${fgWithOpacity}; }`); + } } }); diff --git a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts index e283f72bbe..39eaf7a48b 100644 --- a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts @@ -10,7 +10,7 @@ import { stripWildcards } from 'vs/base/common/strings'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ThrottledDelayer } from 'vs/base/common/async'; -import { getWorkspaceSymbols, IWorkspaceSymbol, IWorkspaceSymbolProvider, IWorkbenchSearchConfiguration } from 'vs/workbench/contrib/search/common/search'; +import { getWorkspaceSymbols, IWorkspaceSymbol, IWorkspaceSymbolProvider } from 'vs/workbench/contrib/search/common/search'; import { SymbolKinds, SymbolTag, SymbolKind } from 'vs/editor/common/modes'; import { ILabelService } from 'vs/platform/label/common/label'; import { Schemas } from 'vs/base/common/network'; @@ -24,6 +24,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { createResourceExcludeMatcher } from 'vs/workbench/services/search/common/search'; import { ResourceMap } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { getSelectionSearchString } from 'vs/editor/contrib/find/findController'; +import { withNullAsUndefined } from 'vs/base/common/types'; interface ISymbolQuickPickItem extends IPickerQuickAccessItem { resource: URI | undefined; @@ -51,29 +54,39 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider().workbench.editor; - const searchConfig = this.configurationService.getValue(); return { openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, - openSideBySideDirection: editorConfig.openSideBySideDirection, - workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter + openSideBySideDirection: editorConfig.openSideBySideDirection }; } protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { - return this.getSymbolPicks(filter, { skipLocal: this.configuration.workspaceSymbolsFilter === 'reduced' }, token); + return this.getSymbolPicks(filter, undefined, token); } async getSymbolPicks(filter: string, options: { skipLocal?: boolean, skipSorting?: boolean, delay?: number } | undefined, token: CancellationToken): Promise> { diff --git a/src/vs/workbench/contrib/search/common/search.ts b/src/vs/workbench/contrib/search/common/search.ts index cd510a3e49..f8fae6dba0 100644 --- a/src/vs/workbench/contrib/search/common/search.ts +++ b/src/vs/workbench/contrib/search/common/search.ts @@ -77,7 +77,9 @@ export interface IWorkbenchSearchConfigurationProperties extends ISearchConfigur quickOpen: { includeSymbols: boolean; includeHistory: boolean; - workspaceSymbolsFilter: 'default' | 'reduced' | 'all'; + history: { + filterSortOrder: 'default' | 'recency' + } }; } @@ -108,8 +110,8 @@ export interface IFilterAndRange { range: IRange; } -export function extractRangeFromFilter(filter: string): IFilterAndRange | undefined { - if (!filter) { +export function extractRangeFromFilter(filter: string, unless?: string[]): IFilterAndRange | undefined { + if (!filter || unless?.some(value => filter.indexOf(value) !== -1)) { return undefined; } diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index 1ed7c150b7..06aff3b33d 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -703,6 +703,8 @@ export class SearchResult extends Disposable { private _rangeHighlightDecorations: RangeHighlightDecorations; private disposePastResults: () => void = () => { }; + private _hasRemovedResults = false; + constructor( private _searchModel: SearchModel, @IReplaceService private readonly replaceService: IReplaceService, @@ -714,6 +716,16 @@ export class SearchResult extends Disposable { this._rangeHighlightDecorations = this.instantiationService.createInstance(RangeHighlightDecorations); this._register(this.modelService.onModelAdded(model => this.onModelAdded(model))); + + this._register(this.onChange(e => { + if (e.removed) { + this._hasRemovedResults = true; + } + })); + } + + get hasRemovedResults(): boolean { + return this._hasRemovedResults; } get query(): ITextQuery | null { @@ -725,7 +737,8 @@ export class SearchResult extends Disposable { const oldFolderMatches = this.folderMatches(); new Promise(resolve => this.disposePastResults = resolve) .then(() => oldFolderMatches.forEach(match => match.clear())) - .then(() => oldFolderMatches.forEach(match => match.dispose())); + .then(() => oldFolderMatches.forEach(match => match.dispose())) + .then(() => this._hasRemovedResults = false); this._rangeHighlightDecorations.removeHighlightRange(); this._folderMatchesMap = TernarySearchTree.forPaths(); @@ -1028,7 +1041,6 @@ export class SearchModel extends Disposable { search(query: ITextQuery, onProgress?: (result: ISearchProgressItem) => void): Promise { this.cancelSearch(true); - this._searchQuery = query; if (!this.searchConfig.searchOnType) { this.searchResult.clear(); diff --git a/src/vs/workbench/contrib/search/test/common/extractRange.test.ts b/src/vs/workbench/contrib/search/test/common/extractRange.test.ts index bffcda4562..d97f9fccf1 100644 --- a/src/vs/workbench/contrib/search/test/common/extractRange.test.ts +++ b/src/vs/workbench/contrib/search/test/common/extractRange.test.ts @@ -42,4 +42,10 @@ suite('extractRangeFromFilter', () => { assert.equal(res?.range.startLineNumber, 19); assert.equal(res?.range.startColumn, 20); }); + + test('unless', async function () { + let res = extractRangeFromFilter('/some/path/file.txt@ (19,20)', ['@']); + + assert.ok(!res); + }); }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 6e79f74ee8..653dd1025d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -381,7 +381,8 @@ const VIEW_CONTAINER = Registry.as(ViewContainerExtensi id: TERMINAL_VIEW_ID, name: nls.localize('terminal', "Terminal"), ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [TERMINAL_VIEW_ID, TERMINAL_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), - focusCommand: { id: TERMINAL_COMMAND_ID.FOCUS } + focusCommand: { id: TERMINAL_COMMAND_ID.FOCUS }, + hideIfEmpty: true }, ViewContainerLocation.Panel); Registry.as(panel.Extensions.Panels).setDefaultPanelId(TERMINAL_VIEW_ID); @@ -389,6 +390,7 @@ Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews id: TERMINAL_VIEW_ID, name: nls.localize('terminal', "Terminal"), canToggleVisibility: false, + canMoveView: true, ctorDescriptor: new SyncDescriptor(TerminalViewPane) }], VIEW_CONTAINER); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 5c4e87ba51..7d31aeabe9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -137,7 +137,7 @@ export class QuickKillTerminalAction extends Action { instance.dispose(true); } await timeout(50); - return this.quickOpenService.show(TERMINAL_PICKER_PREFIX, undefined); + return this.quickOpenService.show(TERMINAL_PICKER_PREFIX); } } @@ -1139,7 +1139,7 @@ export class QuickOpenTermAction extends Action { } public run(): Promise { - return this.quickOpenService.show(TERMINAL_PICKER_PREFIX, undefined); + return this.quickOpenService.show(TERMINAL_PICKER_PREFIX); } } @@ -1160,7 +1160,7 @@ export class RenameTerminalQuickOpenAction extends RenameTerminalAction { await super.run(this.terminal); // This timeout is needed to make sure the previous quickOpen has time to close before we show the next one await timeout(50); - await this.quickOpenService.show(TERMINAL_PICKER_PREFIX, undefined); + await this.quickOpenService.show(TERMINAL_PICKER_PREFIX); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalsQuickAccess.ts b/src/vs/workbench/contrib/terminal/browser/terminalsQuickAccess.ts index fe54c423e6..fd2afd5b35 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalsQuickAccess.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalsQuickAccess.ts @@ -55,7 +55,7 @@ export class TerminalQuickAccessProvider extends PickerQuickAccessProvider= (this.newest?.timestamp ?? 0)) { + this.items.splice(0, 0, ...timeline.items); + } else { + this.items.push(...timeline.items); + } + } else if (timeline.items.length !== 0) { + updated = true; + + this.items.push(...timeline.items); + } + + this._cursor = timeline.paging?.cursor; + + if (updated) { + this.items.sort( + (a, b) => + (b.timestamp - a.timestamp) || + (a.source === undefined + ? b.source === undefined ? 0 : 1 + : b.source === undefined ? -1 : b.source.localeCompare(a.source, undefined, { numeric: true, sensitivity: 'base' })) + ); + } + + return updated; + } + + private _stale = false; + get stale() { + return this._stale; + } + + private _requiresReset = false; + get requiresReset(): boolean { + return this._requiresReset; + } + + invalidate(requiresReset: boolean) { + this._stale = true; + this._requiresReset = requiresReset; + } } export const TimelineFollowActiveEditorContext = new RawContextKey('timelineFollowActiveEditor', true); @@ -91,22 +198,22 @@ export const TimelineFollowActiveEditorContext = new RawContextKey('tim export class TimelinePane extends ViewPane { static readonly TITLE = localize('timeline', "Timeline"); - private _$container!: HTMLElement; - private _$message!: HTMLDivElement; - private _$titleDescription!: HTMLSpanElement; - private _$tree!: HTMLDivElement; - private _tree!: WorkbenchObjectTree; - private _treeRenderer: TimelineTreeRenderer | undefined; + private $container!: HTMLElement; + private $message!: HTMLDivElement; + private $titleDescription!: HTMLSpanElement; + private $tree!: HTMLDivElement; + private tree!: WorkbenchObjectTree; + private treeRenderer: TimelineTreeRenderer | undefined; private commands: TimelinePaneCommands; - private _visibilityDisposables: DisposableStore | undefined; + private visibilityDisposables: DisposableStore | undefined; - private _followActiveEditorContext: IContextKey; + private followActiveEditorContext: IContextKey; - private _excludedSources: Set; - private _cursorsByProvider: Map = new Map(); - private _items: { element: TreeElement }[] = []; - private _pendingRequests = new Map(); - private _uri: URI | undefined; + private excludedSources: Set; + private pendingRequests = new Map(); + private timelinesBySource = new Map(); + + private uri: URI | undefined; constructor( options: IViewPaneOptions, @@ -128,14 +235,13 @@ export class TimelinePane extends ViewPane { this.commands = this._register(this.instantiationService.createInstance(TimelinePaneCommands, this)); - const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); - scopedContextKeyService.createKey('view', TimelinePaneId); + this.followActiveEditorContext = TimelineFollowActiveEditorContext.bindTo(this.contextKeyService); - this._followActiveEditorContext = TimelineFollowActiveEditorContext.bindTo(this.contextKeyService); - - this._excludedSources = new Set(configurationService.getValue('timeline.excludeSources')); + this.excludedSources = new Set(configurationService.getValue('timeline.excludeSources')); configurationService.onDidChangeConfiguration(this.onConfigurationChanged, this); + this._register(timelineService.onDidChangeProviders(this.onProvidersChanged, this)); + this._register(timelineService.onDidChangeTimeline(this.onTimelineChanged, this)); this._register(timelineService.onDidChangeUri(uri => this.setUri(uri), this)); } @@ -149,7 +255,7 @@ export class TimelinePane extends ViewPane { } this._followActiveEditor = value; - this._followActiveEditorContext.set(value); + this.followActiveEditorContext.set(value); if (value) { this.onActiveEditorChanged(); @@ -169,9 +275,9 @@ export class TimelinePane extends ViewPane { this.followActiveEditor = false; } - this._uri = uri; + this.uri = uri; this.titleDescription = uri ? basename(uri.fsPath) : ''; - this._treeRenderer?.setUri(uri); + this.treeRenderer?.setUri(uri); this.loadTimeline(true); } @@ -180,8 +286,15 @@ export class TimelinePane extends ViewPane { return; } - this._excludedSources = new Set(this.configurationService.getValue('timeline.excludeSources')); - this.loadTimeline(true); + this.excludedSources = new Set(this.configurationService.getValue('timeline.excludeSources')); + + const missing = this.timelineService.getSources() + .filter(({ id }) => !this.excludedSources.has(id) && !this.timelinesBySource.has(id)); + if (missing.length !== 0) { + this.loadTimeline(true, missing.map(({ id }) => id)); + } else { + this.refresh(); + } } private onActiveEditorChanged() { @@ -196,9 +309,28 @@ export class TimelinePane extends ViewPane { uri = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); } - if ((uri?.toString(true) === this._uri?.toString(true) && uri !== undefined) || + if ((uri?.toString(true) === this.uri?.toString(true) && uri !== undefined) || // Fallback to match on fsPath if we are dealing with files or git schemes - (uri?.fsPath === this._uri?.fsPath && (uri?.scheme === 'file' || uri?.scheme === 'git') && (this._uri?.scheme === 'file' || this._uri?.scheme === 'git'))) { + (uri?.fsPath === this.uri?.fsPath && (uri?.scheme === 'file' || uri?.scheme === 'git') && (this.uri?.scheme === 'file' || this.uri?.scheme === 'git'))) { + + // If the uri hasn't changed, make sure we have valid caches + for (const source of this.timelineService.getSources()) { + if (this.excludedSources.has(source.id)) { + continue; + } + + const timeline = this.timelinesBySource.get(source.id); + if (timeline !== undefined && !timeline.stale) { + continue; + } + + if (timeline !== undefined) { + this.updateTimeline(timeline, timeline.requiresReset); + } else { + this.loadTimelineForSource(source.id, uri, true); + } + } + return; } @@ -208,8 +340,10 @@ export class TimelinePane extends ViewPane { private onProvidersChanged(e: TimelineProvidersChangeEvent) { if (e.removed) { for (const source of e.removed) { - this.replaceItems(source); + this.timelinesBySource.delete(source); } + + this.refresh(); } if (e.added) { @@ -218,8 +352,17 @@ export class TimelinePane extends ViewPane { } private onTimelineChanged(e: TimelineChangeEvent) { - if (e?.uri === undefined || e.uri.toString(true) !== this._uri?.toString(true)) { - this.loadTimeline(e.reset ?? false, e?.id === undefined ? undefined : [e.id], { before: !e.reset }); + if (e?.uri === undefined || e.uri.toString(true) !== this.uri?.toString(true)) { + const timeline = this.timelinesBySource.get(e.id); + if (timeline === undefined) { + return; + } + + if (this.isBodyVisible()) { + this.updateTimeline(timeline, e.reset ?? false); + } else { + timeline.invalidate(e.reset ?? false); + } } } @@ -230,7 +373,7 @@ export class TimelinePane extends ViewPane { set titleDescription(description: string | undefined) { this._titleDescription = description; - this._$titleDescription.textContent = description ?? ''; + this.$titleDescription.textContent = description ?? ''; } private _message: string | undefined; @@ -252,351 +395,357 @@ export class TimelinePane extends ViewPane { } private showMessage(message: string): void { - DOM.removeClass(this._$message, 'hide'); + DOM.removeClass(this.$message, 'hide'); this.resetMessageElement(); - this._$message.textContent = message; + this.$message.textContent = message; } private hideMessage(): void { this.resetMessageElement(); - DOM.addClass(this._$message, 'hide'); + DOM.addClass(this.$message, 'hide'); } private resetMessageElement(): void { - DOM.clearNode(this._$message); + DOM.clearNode(this.$message); } - private _pendingAnyResults: boolean = false; - private async loadTimeline(reset: boolean, sources?: string[], options: TimelineOptions = {}) { - const defaultPageSize = reset ? InitialPageSize : SubsequentPageSize; + private _isEmpty = true; + private _maxItemCount = 0; + private _visibleItemCount = 0; + private get hasVisibleItems() { + return this._visibleItemCount > 0; + } + + private clear(cancelPending: boolean) { + this._visibleItemCount = 0; + this._maxItemCount = PageSize; + this.timelinesBySource.clear(); + + if (cancelPending) { + for (const { tokenSource } of this.pendingRequests.values()) { + tokenSource.dispose(true); + } + + this.pendingRequests.clear(); + + if (!this.isBodyVisible()) { + this.tree.setChildren(null, undefined); + this._isEmpty = true; + } + } + } + + private async loadTimeline(reset: boolean, sources?: string[]) { // If we have no source, we are reseting all sources, so cancel everything in flight and reset caches if (sources === undefined) { if (reset) { - this._pendingAnyResults = this._pendingAnyResults || this._items.length !== 0; - this._items.length = 0; - this._cursorsByProvider.clear(); - - for (const { tokenSource } of this._pendingRequests.values()) { - tokenSource.dispose(true); - } - - this._pendingRequests.clear(); + this.clear(true); } - // TODO[ECA]: Are these the right the list of schemes to exclude? Is there a better way? - if (this._uri?.scheme === 'vscode-settings' || this._uri?.scheme === 'webview-panel' || this._uri?.scheme === 'walkThrough') { - this._uri = undefined; - this._items.length = 0; + // TODO@eamodio: Are these the right the list of schemes to exclude? Is there a better way? + if (this.uri?.scheme === Schemas.vscodeSettings || this.uri?.scheme === Schemas.webviewPanel || this.uri?.scheme === Schemas.walkThrough) { + this.uri = undefined; + + this.clear(false); this.refresh(); return; } - if (!this._pendingAnyResults && this._uri !== undefined) { + if (this._isEmpty && this.uri !== undefined) { this.setLoadingUriMessage(); } } - if (this._uri === undefined) { - this._items.length = 0; + if (this.uri === undefined) { + this.clear(false); this.refresh(); return; } - const filteredSources = (sources ?? this.timelineService.getSources().map(s => s.id)).filter(s => !this._excludedSources.has(s)); - if (filteredSources.length === 0) { - if (reset) { - this.refresh(); - } - + if (!this.isBodyVisible()) { return; } - let lastIndex = this._items.length - 1; - let lastItem = this._items[lastIndex]?.element; - if (isLoadMoreCommandItem(lastItem)) { - lastItem.themeIcon = { id: 'sync~spin' }; - // this._items.splice(lastIndex, 1); - lastIndex--; + let hasPendingRequests = false; - if (!reset && !options.before) { - lastItem = this._items[lastIndex]?.element; - const selection = [lastItem]; - this._tree.setSelection(selection); - this._tree.setFocus(selection); + for (const source of sources ?? this.timelineService.getSources().map(s => s.id)) { + const requested = this.loadTimelineForSource(source, this.uri, reset); + if (requested) { + hasPendingRequests = true; } } - let noRequests = true; - - for (const source of filteredSources) { - let request = this._pendingRequests.get(source); - - const cursors = this._cursorsByProvider.get(source); - if (!reset) { - // TODO: Handle pending request - - if (cursors?.more !== true) { - continue; - } - - const reusingToken = request?.tokenSource !== undefined; - request = this.timelineService.getTimeline( - source, this._uri, - { - cursor: options.before ? cursors?.startCursors?.before : (cursors?.endCursors ?? cursors?.startCursors)?.after, - ...options, - limit: options.limit === 0 - ? undefined - : options.limit ?? defaultPageSize - }, - request?.tokenSource ?? new CancellationTokenSource(), { cacheResults: true, resetCache: false } - )!; - - if (request === undefined) { - continue; - } - - noRequests = false; - this._pendingRequests.set(source, request); - if (!reusingToken) { - request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source)); - } - } else { - request?.tokenSource.dispose(true); - - request = this.timelineService.getTimeline( - source, this._uri, - { - ...options, - limit: options.limit === 0 - ? undefined - : (reset && cursors?.endCursors?.after !== undefined - ? { cursor: cursors.endCursors.after } - : undefined) ?? options.limit ?? defaultPageSize - }, - new CancellationTokenSource(), { cacheResults: true, resetCache: true } - )!; - - if (request === undefined) { - continue; - } - - noRequests = false; - this._pendingRequests.set(source, request); - request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source)); - } - - this.handleRequest(request); - } - - if (noRequests) { + if (!hasPendingRequests) { this.refresh(); - } else if (this.message !== undefined) { + } else if (this._isEmpty) { this.setLoadingUriMessage(); } } + private loadTimelineForSource(source: string, uri: URI, reset: boolean, options?: TimelineOptions) { + if (this.excludedSources.has(source)) { + return false; + } + + const timeline = this.timelinesBySource.get(source); + + // If we are paging, and there are no more items or we have enough cached items to cover the next page, + // don't bother querying for more + if ( + !reset && + timeline !== undefined && + (!timeline?.more || timeline.items.length > timeline.lastRenderedIndex + PageSize) + ) { + return false; + } + + if (options === undefined) { + options = { cursor: reset ? undefined : timeline?.cursor, limit: PageSize }; + } + + let request = this.pendingRequests.get(source); + if (request !== undefined) { + options.cursor = request.options.cursor; + + // TODO@eamodio deal with concurrent requests better + if (typeof options.limit === 'number') { + if (typeof request.options.limit === 'number') { + options.limit += request.options.limit; + } else { + options.limit = request.options.limit; + } + } + } + request?.tokenSource.dispose(true); + + request = this.timelineService.getTimeline( + source, uri, options, new CancellationTokenSource(), { cacheResults: true, resetCache: reset } + ); + + if (request === undefined) { + return false; + } + + this.pendingRequests.set(source, request); + request.tokenSource.token.onCancellationRequested(() => this.pendingRequests.delete(source)); + + this.handleRequest(request); + + return true; + } + + private updateTimeline(timeline: TimelineAggregate, reset: boolean) { + if (reset) { + this.timelinesBySource.delete(timeline.source); + // Override the limit, to re-query for all our existing cached (possibly visible) items to keep visual continuity + const { oldest } = timeline; + this.loadTimelineForSource(timeline.source, this.uri!, true, oldest !== undefined ? { limit: { timestamp: oldest.timestamp, id: oldest.id } } : undefined); + } else { + // Override the limit, to query for any newer items + const { newest } = timeline; + this.loadTimelineForSource(timeline.source, this.uri!, false, newest !== undefined ? { limit: { timestamp: newest.timestamp, id: newest.id } } : { limit: PageSize }); + } + } + + private _pendingRefresh = false; + private async handleRequest(request: TimelineRequest) { - let timeline: Timeline | undefined; + let response: Timeline | undefined; try { - timeline = await this.progressService.withProgress({ location: this.id }, () => request.result); + response = await this.progressService.withProgress({ location: this.id }, () => request.result); } finally { - this._pendingRequests.delete(request.source); + this.pendingRequests.delete(request.source); } if ( + response === undefined || request.tokenSource.token.isCancellationRequested || - request.uri !== this._uri + request.uri !== this.uri ) { - return; - } - - if (timeline === undefined) { - if (this._pendingRequests.size === 0) { + if (this.pendingRequests.size === 0 && this._pendingRefresh) { this.refresh(); } return; } - let items: TreeElement[]; - const source = request.source; - if (timeline !== undefined) { - if (timeline.paging !== undefined) { - let cursors = this._cursorsByProvider.get(timeline.source ?? source); - if (cursors === undefined) { - cursors = { startCursors: timeline.paging.cursors, more: timeline.paging.more ?? false }; - this._cursorsByProvider.set(timeline.source, cursors); - } else { - if (request.options.before) { - if (cursors.endCursors === undefined) { - cursors.endCursors = cursors.startCursors; - } - cursors.startCursors = timeline.paging.cursors; - } - else { - if (cursors.startCursors === undefined) { - cursors.startCursors = timeline.paging.cursors; - } - cursors.endCursors = timeline.paging.cursors; - } - cursors.more = timeline.paging.more ?? true; - } - } - } else { - this._cursorsByProvider.delete(source); + let updated = false; + const timeline = this.timelinesBySource.get(source); + if (timeline === undefined) { + this.timelinesBySource.set(source, new TimelineAggregate(response)); + updated = true; } - items = (timeline.items as TreeElement[]) ?? []; - - const alreadyHadItems = this._items.length !== 0; - - let changed; - if (request.options.cursor) { - changed = this.mergeItems(request.source, items, request.options); - } else { - changed = this.replaceItems(request.source, items); + else { + updated = timeline.add(response); } - if (!changed) { - // If there are no items at all and no pending requests, make sure to refresh (to show the no timeline info message) - if (this._items.length === 0 && this._pendingRequests.size === 0) { + if (updated) { + this._pendingRefresh = true; + + // If we have visible items already and there are other pending requests, debounce for a bit to wait for other requests + if (this.hasVisibleItems && this.pendingRequests.size !== 0) { + this.refreshDebounced(); + } else { this.refresh(); } - - return; - } - - if (this._pendingRequests.size === 0 && this._items.length !== 0) { - const lastIndex = this._items.length - 1; - const lastItem = this._items[lastIndex]?.element; - - if (timeline.paging?.more || Iterator.some(this._cursorsByProvider.values(), cursors => cursors.more)) { - if (isLoadMoreCommandItem(lastItem)) { - lastItem.themeIcon = undefined; - } - else { - this._items.push({ - element: { - handle: 'vscode-command:loadMore', - label: localize('timeline.loadMore', 'Load more'), - timestamp: 0 - } as CommandItem - }); - } - } - else { - if (isLoadMoreCommandItem(lastItem)) { - this._items.splice(lastIndex, 1); - } - } - } - - // If we have items already and there are other pending requests, debounce for a bit to wait for other requests - if (alreadyHadItems && this._pendingRequests.size !== 0) { - this.refreshDebounced(); - } else { + } else if (this.pendingRequests.size === 0 && this._pendingRefresh) { this.refresh(); } } - private mergeItems(source: string, items: TreeElement[] | undefined, options: TimelineOptions): boolean { - if (items?.length === undefined || items.length === 0) { - return false; + private *getItems(): Generator, any, any> { + let more = false; + + if (this.uri === undefined || this.timelinesBySource.size === 0) { + this._visibleItemCount = 0; + + return; } - if (options.before) { - const ids = new Set(); - const timestamps = new Set(); + const maxCount = this._maxItemCount; + let count = 0; - for (const item of items) { - if (item.id === undefined) { - timestamps.add(item.timestamp); - } - else { - ids.add(item.id); - } + if (this.timelinesBySource.size === 1) { + const [source, timeline] = Iterable.first(this.timelinesBySource); + + timeline.lastRenderedIndex = -1; + + if (this.excludedSources.has(source)) { + this._visibleItemCount = 0; + + return; } - // Remove any duplicate items - // I don't think we need to check all the items, just the most recent page - let i = Math.min(SubsequentPageSize, this._items.length); - let item; - while (i--) { - item = this._items[i].element; - if ( - (item.id === undefined && ids.has(item.id)) || - (item.timestamp === undefined && timestamps.has(item.timestamp)) - ) { - this._items.splice(i, 1); - } + if (timeline.items.length !== 0) { + // If we have any items, just say we have one for now -- the real count will be updated below + this._visibleItemCount = 1; } - this._items.splice(0, 0, ...items.map(item => ({ element: item }))); - } else { - this._items.push(...items.map(item => ({ element: item }))); + more = timeline.more; + + let lastRelativeTime: string | undefined; + for (const item of timeline.items) { + item.relativeTime = undefined; + item.hideRelativeTime = undefined; + + count++; + if (count > maxCount) { + more = true; + break; + } + + lastRelativeTime = updateRelativeTime(item, lastRelativeTime); + yield { element: item }; + } + + timeline.lastRenderedIndex = count - 1; + } + else { + const sources: { timeline: TimelineAggregate; iterator: IterableIterator; nextItem: IteratorResult }[] = []; + + let hasAnyItems = false; + let mostRecentEnd = 0; + + for (const [source, timeline] of this.timelinesBySource) { + timeline.lastRenderedIndex = -1; + + if (this.excludedSources.has(source) || timeline.stale) { + continue; + } + + if (timeline.items.length !== 0) { + hasAnyItems = true; + } + + if (timeline.more) { + more = true; + + const last = timeline.items[Math.min(maxCount, timeline.items.length - 1)]; + if (last.timestamp > mostRecentEnd) { + mostRecentEnd = last.timestamp; + } + } + + const iterator = timeline.items[Symbol.iterator](); + sources.push({ timeline: timeline, iterator: iterator, nextItem: iterator.next() }); + } + + this._visibleItemCount = hasAnyItems ? 1 : 0; + + function getNextMostRecentSource() { + return sources + .filter(source => !source.nextItem!.done) + .reduce((previous, current) => (previous === undefined || current.nextItem!.value.timestamp >= previous.nextItem!.value.timestamp) ? current : previous, undefined!); + } + + let lastRelativeTime: string | undefined; + let nextSource; + while (nextSource = getNextMostRecentSource()) { + nextSource.timeline.lastRenderedIndex++; + + const item = nextSource.nextItem.value; + item.relativeTime = undefined; + item.hideRelativeTime = undefined; + + if (item.timestamp >= mostRecentEnd) { + count++; + if (count > maxCount) { + more = true; + break; + } + + lastRelativeTime = updateRelativeTime(item, lastRelativeTime); + yield { element: item }; + } + + nextSource.nextItem = nextSource.iterator.next(); + } } - this.sortItems(); - return true; - } + this._visibleItemCount = count; - private replaceItems(source: string, items?: TreeElement[]): boolean { - if (items?.length) { - this._items.splice( - 0, this._items.length, - ...this._items.filter(item => item.element.source !== source), - ...items.map(item => ({ element: item })) - ); - this.sortItems(); - - return true; + if (more) { + yield { + element: { + handle: 'vscode-command:loadMore', + label: localize('timeline.loadMore', 'Load more'), + timestamp: 0 + } as CommandItem + }; } - - if (this._items.length && this._items.some(item => item.element.source === source)) { - this._items = this._items.filter(item => item.element.source !== source); - - return true; - } - - return false; - } - - private sortItems() { - this._items.sort( - (a, b) => - (b.element.timestamp - a.element.timestamp) || - (a.element.source === undefined - ? b.element.source === undefined ? 0 : 1 - : b.element.source === undefined ? -1 : b.element.source.localeCompare(a.element.source, undefined, { numeric: true, sensitivity: 'base' })) - ); - } private refresh() { - if (this._uri === undefined) { + if (!this.isBodyVisible()) { + return; + } + + this.tree.setChildren(null, this.getItems() as any); + this._isEmpty = !this.hasVisibleItems; + + if (this.uri === undefined) { this.titleDescription = undefined; this.message = localize('timeline.editorCannotProvideTimeline', 'The active editor cannot provide timeline information.'); - } else if (this._items.length === 0) { - if (this._pendingRequests.size !== 0) { + } else if (this._isEmpty) { + if (this.pendingRequests.size !== 0) { this.setLoadingUriMessage(); } else { - this.titleDescription = basename(this._uri.fsPath); + this.titleDescription = basename(this.uri.fsPath); this.message = localize('timeline.noTimelineInfo', 'No timeline information was provided.'); } } else { - this.titleDescription = basename(this._uri.fsPath); + this.titleDescription = basename(this.uri.fsPath); this.message = undefined; } - this._pendingAnyResults = false; - this._tree.setChildren(null, this._items); + this._pendingRefresh = false; } @debounce(500) @@ -606,55 +755,65 @@ export class TimelinePane extends ViewPane { focus(): void { super.focus(); - this._tree.domFocus(); + this.tree.domFocus(); + } + + setExpanded(expanded: boolean): boolean { + const changed = super.setExpanded(expanded); + + if (changed && this.isBodyVisible()) { + this.onActiveEditorChanged(); + } + + return changed; } setVisible(visible: boolean): void { if (visible) { - this._visibilityDisposables = new DisposableStore(); + this.visibilityDisposables = new DisposableStore(); - this.timelineService.onDidChangeProviders(this.onProvidersChanged, this, this._visibilityDisposables); - this.timelineService.onDidChangeTimeline(this.onTimelineChanged, this, this._visibilityDisposables); - this.editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this, this._visibilityDisposables); + this.editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this, this.visibilityDisposables); + // Refresh the view on focus to update the relative timestamps + this.onDidFocus(() => this.refreshDebounced(), this, this.visibilityDisposables); this.onActiveEditorChanged(); } else { - this._visibilityDisposables?.dispose(); + this.visibilityDisposables?.dispose(); } super.setVisible(visible); } protected layoutBody(height: number, width: number): void { - this._tree.layout(height, width); + this.tree.layout(height, width); } protected renderHeaderTitle(container: HTMLElement): void { super.renderHeaderTitle(container, this.title); DOM.addClass(container, 'timeline-view'); - this._$titleDescription = DOM.append(container, DOM.$('span.description', undefined, this.titleDescription ?? '')); + this.$titleDescription = DOM.append(container, DOM.$('span.description', undefined, this.titleDescription ?? '')); } protected renderBody(container: HTMLElement): void { super.renderBody(container); - this._$container = container; + this.$container = container; DOM.addClasses(container, 'tree-explorer-viewlet-tree-view', 'timeline-tree-view'); - this._$message = DOM.append(this._$container, DOM.$('.message')); - DOM.addClass(this._$message, 'timeline-subtle'); + this.$message = DOM.append(this.$container, DOM.$('.message')); + DOM.addClass(this.$message, 'timeline-subtle'); this.message = localize('timeline.editorCannotProvideTimeline', 'The active editor cannot provide timeline information.'); - this._$tree = document.createElement('div'); - DOM.addClasses(this._$tree, 'customview-tree', 'file-icon-themable-tree', 'hide-arrows'); - // DOM.addClass(this._treeElement, 'show-file-icons'); - container.appendChild(this._$tree); + this.$tree = document.createElement('div'); + DOM.addClasses(this.$tree, 'customview-tree', 'file-icon-themable-tree', 'hide-arrows'); + // DOM.addClass(this.treeElement, 'show-file-icons'); + container.appendChild(this.$tree); - this._treeRenderer = this.instantiationService.createInstance(TimelineTreeRenderer, this.commands); - this._tree = >this.instantiationService.createInstance(WorkbenchObjectTree, 'TimelinePane', - this._$tree, new TimelineListVirtualDelegate(), [this._treeRenderer], { + this.treeRenderer = this.instantiationService.createInstance(TimelineTreeRenderer, this.commands); + this.tree = >this.instantiationService.createInstance(WorkbenchObjectTree, 'TimelinePane', + this.$tree, new TimelineListVirtualDelegate(), [this.treeRenderer], { identityProvider: new TimelineIdentityProvider(), keyboardNavigationLabelProvider: new TimelineKeyboardNavigationLabelProvider(), overrideStyles: { @@ -663,17 +822,17 @@ export class TimelinePane extends ViewPane { } }); - const customTreeNavigator = ResourceNavigator.createTreeResourceNavigator(this._tree, { openOnFocus: false, openOnSelection: false }); + const customTreeNavigator = ResourceNavigator.createTreeResourceNavigator(this.tree, { openOnFocus: false, openOnSelection: false }); this._register(customTreeNavigator); - this._register(this._tree.onContextMenu(e => this.onContextMenu(this.commands, e))); - this._register(this._tree.onDidChangeSelection(e => this.ensureValidItems())); + this._register(this.tree.onContextMenu(e => this.onContextMenu(this.commands, e))); + this._register(this.tree.onDidChangeSelection(e => this.ensureValidItems())); this._register( customTreeNavigator.onDidOpenResource(e => { if (!e.browserEvent || !this.ensureValidItems()) { return; } - const selection = this._tree.getSelection(); + const selection = this.tree.getSelection(); const item = selection.length === 1 ? selection[0] : undefined; // eslint-disable-next-line eqeqeq if (item == null) { @@ -686,23 +845,24 @@ export class TimelinePane extends ViewPane { } } else if (isLoadMoreCommandItem(item)) { - // TODO: Change this, but right now this is the pending signal - if (item.themeIcon !== undefined) { + if (this.pendingRequests.size !== 0) { return; } + this._maxItemCount = this._visibleItemCount + PageSize; this.loadTimeline(false); } }) ); } ensureValidItems() { - if (this._pendingAnyResults) { - this._tree.setChildren(null, undefined); + // If we don't have any non-excluded timelines, clear the tree and show the loading message + if (!this.hasVisibleItems || !this.timelineService.getSources().some(({ id }) => !this.excludedSources.has(id) && this.timelinesBySource.has(id))) { + this.tree.setChildren(null, undefined); + this._isEmpty = true; this.setLoadingUriMessage(); - this._pendingAnyResults = false; return false; } @@ -710,7 +870,7 @@ export class TimelinePane extends ViewPane { } setLoadingUriMessage() { - const file = this._uri && basename(this._uri.fsPath); + const file = this.uri && basename(this.uri.fsPath); this.titleDescription = file ?? ''; this.message = file ? localize('timeline.loading', 'Loading timeline for {0}...', file) : ''; } @@ -729,7 +889,7 @@ export class TimelinePane extends ViewPane { return; } - this._tree.setFocus([item]); + this.tree.setFocus([item]); const actions = commands.getItemContextActions(item); if (!actions.length) { return; @@ -747,10 +907,10 @@ export class TimelinePane extends ViewPane { }, onHide: (wasCancelled?: boolean) => { if (wasCancelled) { - this._tree.domFocus(); + this.tree.domFocus(); } }, - getActionsContext: (): TimelineActionContext => ({ uri: this._uri, item: item }), + getActionsContext: (): TimelineActionContext => ({ uri: this.uri, item: item }), actionRunner: new TimelineActionRunner() }); } @@ -800,7 +960,7 @@ class TimelineActionRunner extends ActionRunner { runAction(action: IAction, { uri, item }: TimelineActionContext): Promise { if (!isTimelineItem(item)) { - // TODO + // TODO@eamodio do we need to do anything else? return action.run(); } @@ -836,25 +996,25 @@ export class TimelineListVirtualDelegate implements IListVirtualDelegate { readonly templateId: string = TimelineElementTemplate.id; - private _actionViewItemProvider: IActionViewItemProvider; + private actionViewItemProvider: IActionViewItemProvider; constructor( private readonly commands: TimelinePaneCommands, @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IThemeService private _themeService: IThemeService + @IThemeService private themeService: IThemeService ) { - this._actionViewItemProvider = (action: IAction) => action instanceof MenuItemAction + this.actionViewItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action) : undefined; } - private _uri: URI | undefined; + private uri: URI | undefined; setUri(uri: URI | undefined) { - this._uri = uri; + this.uri = uri; } renderTemplate(container: HTMLElement): TimelineElementTemplate { - return new TimelineElementTemplate(container, this._actionViewItemProvider); + return new TimelineElementTemplate(container, this.actionViewItemProvider); } renderElement( @@ -867,7 +1027,7 @@ class TimelineTreeRenderer implements ITreeRenderer /^inline/.test(g)); menu.dispose(); - contextKeyService.dispose(); + scoped.dispose(); return result; } diff --git a/src/vs/workbench/contrib/timeline/common/timeline.ts b/src/vs/workbench/contrib/timeline/common/timeline.ts index df79b7e36d..97d93ac2a4 100644 --- a/src/vs/workbench/contrib/timeline/common/timeline.ts +++ b/src/vs/workbench/contrib/timeline/common/timeline.ts @@ -31,18 +31,20 @@ export interface TimelineItem { detail?: string; command?: Command; contextValue?: string; + + relativeTime?: string; + hideRelativeTime?: boolean; } export interface TimelineChangeEvent { - id?: string; + id: string; uri?: URI; reset?: boolean } export interface TimelineOptions { cursor?: string; - before?: boolean; - limit?: number | { cursor: string }; + limit?: number | { timestamp: number; id?: string }; } export interface InternalTimelineOptions { @@ -55,11 +57,7 @@ export interface Timeline { items: TimelineItem[]; paging?: { - cursors: { - before: string; - after?: string - }; - more?: boolean; + cursor: string | undefined; } } diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index d67735497a..b67c6a0eb4 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -23,20 +23,80 @@ export class TimelineService implements ITimelineService { private readonly _onDidChangeUri = new Emitter(); readonly onDidChangeUri: Event = this._onDidChangeUri.event; - private readonly _providers = new Map(); - private readonly _providerSubscriptions = new Map(); + private readonly providers = new Map(); + private readonly providerSubscriptions = new Map(); constructor( @ILogService private readonly logService: ILogService, @IViewsService protected viewsService: IViewsService, ) { + // let source = 'fast-source'; + // this.registerTimelineProvider({ + // scheme: '*', + // id: source, + // label: 'Fast Source', + // provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean | undefined; }) { + // if (options.cursor === undefined) { + // return Promise.resolve({ + // source: source, + // items: [ + // { + // handle: `${source}|1`, + // id: '1', + // label: 'Fast Timeline1', + // description: '', + // timestamp: Date.now(), + // source: source + // }, + // { + // handle: `${source}|2`, + // id: '2', + // label: 'Fast Timeline2', + // description: '', + // timestamp: Date.now() - 3000000000, + // source: source + // } + // ], + // paging: { + // cursor: 'next' + // } + // }); + // } + // return Promise.resolve({ + // source: source, + // items: [ + // { + // handle: `${source}|3`, + // id: '3', + // label: 'Fast Timeline3', + // description: '', + // timestamp: Date.now() - 4000000000, + // source: source + // }, + // { + // handle: `${source}|4`, + // id: '4', + // label: 'Fast Timeline4', + // description: '', + // timestamp: Date.now() - 300000000000, + // source: source + // } + // ], + // paging: { + // cursor: undefined + // } + // }); + // }, + // dispose() { } + // }); + // let source = 'slow-source'; // this.registerTimelineProvider({ // scheme: '*', // id: source, // label: 'Slow Source', // provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean | undefined; }) { - // return new Promise(resolve => setTimeout(() => { + // return new Promise(resolve => setTimeout(() => { // resolve({ // source: source, // items: [ @@ -69,7 +129,7 @@ export class TimelineService implements ITimelineService { // id: source, // label: 'Very Slow Source', // provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean | undefined; }) { - // return new Promise(resolve => setTimeout(() => { + // return new Promise(resolve => setTimeout(() => { // resolve({ // source: source, // items: [ @@ -98,13 +158,13 @@ export class TimelineService implements ITimelineService { } getSources() { - return [...this._providers.values()].map(p => ({ id: p.id, label: p.label })); + return [...this.providers.values()].map(p => ({ id: p.id, label: p.label })); } getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: InternalTimelineOptions) { this.logService.trace(`TimelineService#getTimeline(${id}): uri=${uri.toString(true)}`); - const provider = this._providers.get(id); + const provider = this.providers.get(id); if (provider === undefined) { return undefined; } @@ -141,10 +201,10 @@ export class TimelineService implements ITimelineService { const id = provider.id; - const existing = this._providers.get(id); + const existing = this.providers.get(id); if (existing) { // For now to deal with https://github.com/microsoft/vscode/issues/89553 allow any overwritting here (still will be blocked in the Extension Host) - // TODO[ECA]: Ultimately will need to figure out a way to unregister providers when the Extension Host restarts/crashes + // TODO@eamodio: Ultimately will need to figure out a way to unregister providers when the Extension Host restarts/crashes // throw new Error(`Timeline Provider ${id} already exists.`); try { existing?.dispose(); @@ -152,15 +212,15 @@ export class TimelineService implements ITimelineService { catch { } } - this._providers.set(id, provider); + this.providers.set(id, provider); if (provider.onDidChange) { - this._providerSubscriptions.set(id, provider.onDidChange(e => this._onDidChangeTimeline.fire(e))); + this.providerSubscriptions.set(id, provider.onDidChange(e => this._onDidChangeTimeline.fire(e))); } this._onDidChangeProviders.fire({ added: [id] }); return { dispose: () => { - this._providers.delete(id); + this.providers.delete(id); this._onDidChangeProviders.fire({ removed: [id] }); } }; @@ -169,12 +229,12 @@ export class TimelineService implements ITimelineService { unregisterTimelineProvider(id: string): void { this.logService.trace(`TimelineService#unregisterTimelineProvider: id=${id}`); - if (!this._providers.has(id)) { + if (!this.providers.has(id)) { return; } - this._providers.delete(id); - this._providerSubscriptions.delete(id); + this.providers.delete(id); + this.providerSubscriptions.delete(id); this._onDidChangeProviders.fire({ removed: [id] }); } diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 3484bf0647..6b9fa6556e 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -190,7 +190,7 @@ export class ReleaseNotesManager { body { padding: 10px 20px; line-height: 22px; - max-width: 780px; + max-width: 882px; margin: 0 auto; } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index eb4f396e3e..d33ce3287f 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -15,7 +15,7 @@ import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import type { IEditorContribution } from 'vs/editor/common/editorCommon'; import type { ITextModel } from 'vs/editor/common/model'; -import { AuthenticationSession } from 'vs/editor/common/modes'; +import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -63,6 +63,8 @@ const enum AuthStatus { const CONTEXT_AUTH_TOKEN_STATE = new RawContextKey('authTokenStatus', AuthStatus.Initializing); const CONTEXT_CONFLICTS_SOURCES = new RawContextKey('conflictsSources', ''); +const USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY = 'userDataSyncAccountPreference'; + type ConfigureSyncQuickPickItem = { id: SyncResource, label: string, description?: string }; function getSyncAreaLabel(source: SyncResource): string { @@ -195,18 +197,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo return; } - const selectedAccount = await this.quickInputService.pick(sessions.map(session => { - return { - id: session.id, - label: session.accountName - }; - }), { canPickMany: false }); - - if (selectedAccount) { - const selected = sessions.filter(account => selectedAccount.id === account.id)[0]; - this.logAuthenticatedEvent(selected); - await this.setActiveAccount(selected); + const accountPreference = this.storageService.get(USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY, StorageScope.GLOBAL); + if (accountPreference) { + const matchingSession = sessions.find(session => session.id === accountPreference); + if (matchingSession) { + this.setActiveAccount(matchingSession); + return; + } } + + await this.showSwitchAccountPicker(sessions); } private logAuthenticatedEvent(session: AuthenticationSession): void { @@ -246,15 +246,80 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.updateBadge(); } - private async onDidChangeSessions(providerId: string): Promise { + private async showSwitchAccountPicker(sessions: readonly AuthenticationSession[]): Promise { + return new Promise((resolve, _) => { + const quickPick = this.quickInputService.createQuickPick<{ label: string, session: AuthenticationSession }>(); + quickPick.title = localize('chooseAccountTitle', "Sync: Choose Account"); + quickPick.placeholder = localize('chooseAccount', "Choose an account you would like to use for settings sync"); + quickPick.items = sessions.map(session => { + return { + label: session.accountName, + session: session + }; + }); + + quickPick.onDidHide(() => { + quickPick.dispose(); + resolve(); + }); + + quickPick.onDidAccept(() => { + const selected = quickPick.selectedItems[0]; + this.setActiveAccount(selected.session); + this.storageService.store(USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY, selected.session.id, StorageScope.GLOBAL); + quickPick.dispose(); + resolve(); + }); + + quickPick.show(); + }); + } + + private async onDidChangeSessions(e: { providerId: string, event: AuthenticationSessionsChangeEvent }): Promise { + const { providerId, event } = e; if (providerId === this.userDataSyncStore!.authenticationProviderId) { if (this.activeAccount) { - // Try to update existing account, case where access token has been refreshed - const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []); - const matchingAccount = accounts.filter(a => a.id === this.activeAccount?.id)[0]; - this.setActiveAccount(matchingAccount); + if (event.removed.length) { + const activeWasRemoved = !!event.removed.find(removed => removed === this.activeAccount!.id); + + // If the current account was removed, check if another account can be used, otherwise offer to turn off sync + if (activeWasRemoved) { + const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []); + if (accounts.length) { + // Show switch dialog here + await this.showSwitchAccountPicker(accounts); + } else { + await this.turnOff(); + this.setActiveAccount(undefined); + return; + } + + } + } + + if (event.added.length) { + // Offer to switch accounts + const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []); + await this.showSwitchAccountPicker(accounts); + return; + } + + if (event.changed.length) { + const activeWasChanged = !!event.changed.find(changed => changed === this.activeAccount!.id); + if (activeWasChanged) { + // Try to update existing account, case where access token has been refreshed + const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []); + const matchingAccount = accounts.filter(a => a.id === this.activeAccount?.id)[0]; + this.setActiveAccount(matchingAccount); + } + } } else { - this.initializeActiveAccount(); + await this.initializeActiveAccount(); + + // If logged in for the first time from accounts menu, prompt if sync should be turned on + if (this.activeAccount) { + this.turnOn(true); + } } } } @@ -520,7 +585,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private async turnOn(): Promise { + private async turnOn(skipAccountPick?: boolean): Promise { if (!this.storageService.getBoolean('sync.donotAskPreviewConfirmation', StorageScope.GLOBAL, false)) { const result = await this.dialogService.show( Severity.Info, @@ -562,7 +627,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo disposables.add(Event.any(quickPick.onDidAccept, quickPick.onDidCustom)(async () => { if (quickPick.selectedItems.length) { this.updateConfiguration(items, quickPick.selectedItems); - this.doTurnOn().then(c, e); + this.doTurnOn(skipAccountPick).then(c, e); quickPick.hide(); } })); @@ -571,8 +636,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } - private async doTurnOn(): Promise { - if (this.authenticationState.get() === AuthStatus.SignedIn) { + private async doTurnOn(skipAccountPick?: boolean): Promise { + if (this.authenticationState.get() === AuthStatus.SignedIn && !skipAccountPick) { await new Promise((c, e) => { const disposables: DisposableStore = new DisposableStore(); const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId); @@ -706,7 +771,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async turnOff(): Promise { const result = await this.dialogService.confirm({ type: 'info', - message: localize('turn off sync confirmation', "Turn off Sync"), + message: localize('turn off sync confirmation', "Do you want to turn off sync?"), detail: localize('turn off sync detail', "Your settings, keybindings, extensions and UI State will no longer be synced."), primaryButton: localize('turn off', "Turn Off"), checkbox: { @@ -750,14 +815,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private getConflictsEditorInputs(syncResource: SyncResource): DiffEditorInput[] { return this.editorService.editors.filter(input => { const resource = input instanceof DiffEditorInput ? input.master.resource : input.resource; - return getSyncResourceFromLocalPreview(resource!, this.workbenchEnvironmentService) === syncResource; + return resource && getSyncResourceFromLocalPreview(resource!, this.workbenchEnvironmentService) === syncResource; }) as DiffEditorInput[]; } private getAllConflictsEditorInputs(): IEditorInput[] { return this.editorService.editors.filter(input => { const resource = input instanceof DiffEditorInput ? input.master.resource : input.resource; - return getSyncResourceFromLocalPreview(resource!, this.workbenchEnvironmentService) !== undefined; + return resource && getSyncResourceFromLocalPreview(resource!, this.workbenchEnvironmentService) !== undefined; }); } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts index 5172161477..6886850deb 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts @@ -10,13 +10,12 @@ import { localize } from 'vs/nls'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TreeViewPane, TreeView } from 'vs/workbench/browser/parts/views/treeView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ALL_SYNC_RESOURCES, CONTEXT_SYNC_ENABLEMENT, IUserDataSyncStoreService, toRemoteBackupSyncResource, resolveBackupSyncResource, IUserDataSyncBackupStoreService, IResourceRefHandle, toLocalBackupSyncResource, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { ALL_SYNC_RESOURCES, CONTEXT_SYNC_ENABLEMENT, SyncResource, IUserDataSyncService, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService, RawContextKey, ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { FolderThemeIcon, FileThemeIcon } from 'vs/platform/theme/common/themeService'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { FolderThemeIcon } from 'vs/platform/theme/common/themeService'; import { fromNow } from 'vs/base/common/date'; import { pad } from 'vs/base/common/strings'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; @@ -26,8 +25,7 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, - @IUserDataSyncBackupStoreService private readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, + @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, ) { const container = this.registerSyncViewContainer(); this.registerBackupView(container, true); @@ -59,9 +57,7 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { const disposable = treeView.onDidChangeVisibility(visible => { if (visible && !treeView.dataProvider) { disposable.dispose(); - treeView.dataProvider = this.instantiationService.createInstance(UserDataSyncHistoryViewDataProvider, id, - (resource: SyncResource) => remote ? this.userDataSyncStoreService.getAllRefs(resource) : this.userDataSyncBackupStoreService.getAllRefs(resource), - (resource: SyncResource, ref: string) => remote ? toRemoteBackupSyncResource(resource, ref) : toLocalBackupSyncResource(resource, ref)); + treeView.dataProvider = new UserDataSyncHistoryViewDataProvider(remote, this.userDataSyncService); } }); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); @@ -104,28 +100,11 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { registerAction2(class extends Action2 { constructor() { super({ - id: `workbench.actions.sync.${viewId}.resolveResourceRef`, - title: localize('workbench.actions.sync.resolveResourceRef', "Resolve Resource Ref"), - }); - } - async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { - const editorService = accessor.get(IEditorService); - let resource = URI.parse(handle.$treeItemHandle); - const result = resolveBackupSyncResource(resource); - if (result) { - resource = resource.with({ fragment: result.resource }); - await editorService.openEditor({ resource }); - } - } - }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.actions.sync.${viewId}.resolveResourceRefCompletely`, - title: localize('workbench.actions.sync.resolveResourceRefCompletely', "Show full content"), + id: `workbench.actions.sync.resolveResource`, + title: localize('workbench.actions.sync.resolveResourceRef', "Show full content"), menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', viewId), ContextKeyExpr.regex('viewItem', /syncref-.*/i)) + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', viewId), ContextKeyExpr.regex('viewItem', /sync-resource-.*/i)) }, }); } @@ -134,34 +113,28 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { await editorService.openEditor({ resource: URI.parse(handle.$treeItemHandle) }); } }); + registerAction2(class extends Action2 { constructor() { super({ - id: `workbench.actions.${viewId}.commpareWithLocal`, - title: localize('workbench.action.deleteRef', "Open Changes"), - menu: { - id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', viewId), ContextKeyExpr.regex('viewItem', /syncref-(settings|keybindings).*/i)) - }, + id: `workbench.actions.sync.commpareWithLocal`, + title: localize('workbench.actions.sync.commpareWithLocal', "Open Changes"), }); } async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { const editorService = accessor.get(IEditorService); - const environmentService = accessor.get(IEnvironmentService); - const resource = URI.parse(handle.$treeItemHandle); - const result = resolveBackupSyncResource(resource); - if (result) { - const leftResource: URI = resource.with({ fragment: result.resource }); - const rightResource: URI = result.resource === 'settings' ? environmentService.settingsResource : environmentService.keybindingsResource; + const { resource, comparableResource } = <{ resource: string, comparableResource?: string }>JSON.parse(handle.$treeItemHandle); + if (comparableResource) { await editorService.openEditor({ - leftResource, - rightResource, + leftResource: URI.parse(resource), + rightResource: URI.parse(comparableResource), options: { - preserveFocus: false, - pinned: true, + preserveFocus: true, revealIfVisible: true, }, }); + } else { + await editorService.openEditor({ resource: URI.parse(resource) }); } } }); @@ -169,48 +142,55 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { } +interface SyncResourceTreeItem extends ITreeItem { + resource: SyncResource; + resourceHandle: ISyncResourceHandle; +} + class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvider { - constructor( - private readonly viewId: string, - private getAllRefs: (resource: SyncResource) => Promise, - private toResource: (resource: SyncResource, ref: string) => URI - ) { - } + constructor(private readonly remote: boolean, private userDataSyncService: IUserDataSyncService) { } async getChildren(element?: ITreeItem): Promise { - if (element) { - return this.getResources(element.handle); + if (!element) { + return ALL_SYNC_RESOURCES.map(resourceKey => ({ + handle: resourceKey, + collapsibleState: TreeItemCollapsibleState.Collapsed, + label: { label: resourceKey }, + themeIcon: FolderThemeIcon, + })); } - return ALL_SYNC_RESOURCES.map(resourceKey => ({ - handle: resourceKey, - collapsibleState: TreeItemCollapsibleState.Collapsed, - label: { label: resourceKey }, - themeIcon: FolderThemeIcon, - contextValue: `sync-${resourceKey}` - })); - } - - private async getResources(handle: string): Promise { - const resourceKey = ALL_SYNC_RESOURCES.filter(key => key === handle)[0]; + const resourceKey = ALL_SYNC_RESOURCES.filter(key => key === element.handle)[0] as SyncResource; if (resourceKey) { - const refHandles = await this.getAllRefs(resourceKey); - return refHandles.map(({ ref, created }) => { - const handle = this.toResource(resourceKey, ref).toString(); + const refHandles = this.remote ? await this.userDataSyncService.getRemoteSyncResourceHandles(resourceKey) : await this.userDataSyncService.getLocalSyncResourceHandles(resourceKey); + return refHandles.map(({ uri, created }) => { + return { + handle: uri.toString(), + collapsibleState: TreeItemCollapsibleState.Collapsed, + label: { label: label(new Date(created)) }, + description: fromNow(created, true), + resourceUri: uri, + resource: resourceKey, + resourceHandle: { uri, created }, + contextValue: `sync-resource-${resourceKey}` + }; + }); + } + if ((element).resourceHandle) { + const associatedResources = await this.userDataSyncService.getAssociatedResources((element).resource, (element).resourceHandle); + return associatedResources.map(({ resource, comparableResource }) => { + const handle = JSON.stringify({ resource: resource.toString(), comparableResource: comparableResource?.toString() }); return { handle, collapsibleState: TreeItemCollapsibleState.None, - label: { label: label(new Date(created)) }, - description: fromNow(created, true), - command: { id: `workbench.actions.sync.${this.viewId}.resolveResourceRef`, title: '', arguments: [{ $treeItemHandle: handle, $treeViewId: '' }] }, - themeIcon: FileThemeIcon, - contextValue: `syncref-${resourceKey}` + resourceUri: resource, + command: { id: `workbench.actions.sync.commpareWithLocal`, title: '', arguments: [{ $treeViewId: '', $treeItemHandle: handle }] }, + contextValue: `sync-associatedResource-${(element).resource}` }; }); } return []; } - } function label(date: Date): string { diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts index 4c0ece4212..5f062b6ee0 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts @@ -7,8 +7,7 @@ import { Lazy } from 'vs/base/common/lazy'; import { URI } from 'vs/base/common/uri'; import { EditorInput, GroupIdentifier, IEditorInput, Verbosity } from 'vs/workbench/common/editor'; import { IWebviewService, WebviewIcons, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; - -const WebviewPanelResourceScheme = 'webview-panel'; +import { Schemas } from 'vs/base/common/network'; export class WebviewInput extends EditorInput { @@ -24,7 +23,7 @@ export class WebviewInput extends EditorInput { get resource() { return URI.from({ - scheme: WebviewPanelResourceScheme, + scheme: Schemas.webviewPanel, path: `webview-panel/webview-${this.id}` }); } diff --git a/src/vs/workbench/electron-browser/actions/windowActions.ts b/src/vs/workbench/electron-browser/actions/windowActions.ts index 703b731ffd..1297d15104 100644 --- a/src/vs/workbench/electron-browser/actions/windowActions.ts +++ b/src/vs/workbench/electron-browser/actions/windowActions.ts @@ -34,10 +34,8 @@ export class CloseCurrentWindowAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.electronService.closeWindow(); - - return Promise.resolve(true); } } @@ -91,10 +89,8 @@ export class ZoomInAction extends BaseZoomAction { super(id, label, configurationService); } - run(): Promise { + async run(): Promise { this.setConfiguredZoomLevel(webFrame.getZoomLevel() + 1); - - return Promise.resolve(true); } } @@ -111,10 +107,8 @@ export class ZoomOutAction extends BaseZoomAction { super(id, label, configurationService); } - run(): Promise { + async run(): Promise { this.setConfiguredZoomLevel(webFrame.getZoomLevel() - 1); - - return Promise.resolve(true); } } @@ -131,10 +125,8 @@ export class ZoomResetAction extends BaseZoomAction { super(id, label, configurationService); } - run(): Promise { + async run(): Promise { this.setConfiguredZoomLevel(0); - - return Promise.resolve(true); } } @@ -160,8 +152,8 @@ export class ReloadWindowWithExtensionsDisabledAction extends Action { export abstract class BaseSwitchWindow extends Action { - private closeWindowAction: IQuickInputButton = { - iconClass: 'action-remove-from-recently-opened', + private readonly closeWindowAction: IQuickInputButton = { + iconClass: 'codicon-close', tooltip: nls.localize('close', "Close Window") }; @@ -204,7 +196,7 @@ export abstract class BaseSwitchWindow extends Action { placeHolder, quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, onDidTriggerItemButton: async context => { - await this.electronService.closeWindow(); + await this.electronService.closeWindowById(context.item.payload); context.removeItem(); } }); diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 7a917e230b..3775e5d9a0 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -3,12 +3,14 @@ * 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 { Disposable } from 'vs/base/common/lifecycle'; -import { AuthenticationSession } from 'vs/editor/common/modes'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { MainThreadAuthenticationProvider } from 'vs/workbench/api/browser/mainThreadAuthentication'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; export const IAuthenticationService = createDecorator('IAuthenticationService'); @@ -17,12 +19,12 @@ export interface IAuthenticationService { registerAuthenticationProvider(id: string, provider: MainThreadAuthenticationProvider): void; unregisterAuthenticationProvider(id: string): void; - sessionsUpdate(providerId: string): void; + sessionsUpdate(providerId: string, event: AuthenticationSessionsChangeEvent): void; readonly onDidRegisterAuthenticationProvider: Event; readonly onDidUnregisterAuthenticationProvider: Event; - readonly onDidChangeSessions: Event; + readonly onDidChangeSessions: Event<{ providerId: string, event: AuthenticationSessionsChangeEvent }>; getSessions(providerId: string): Promise | undefined>; getDisplayName(providerId: string): string; login(providerId: string, scopes: string[]): Promise; @@ -31,6 +33,7 @@ export interface IAuthenticationService { export class AuthenticationService extends Disposable implements IAuthenticationService { _serviceBrand: undefined; + private _placeholderMenuItem: IDisposable | undefined; private _authenticationProviders: Map = new Map(); @@ -40,25 +43,53 @@ export class AuthenticationService extends Disposable implements IAuthentication private _onDidUnregisterAuthenticationProvider: Emitter = this._register(new Emitter()); readonly onDidUnregisterAuthenticationProvider: Event = this._onDidUnregisterAuthenticationProvider.event; - private _onDidChangeSessions: Emitter = this._register(new Emitter()); - readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; + private _onDidChangeSessions: Emitter<{ providerId: string, event: AuthenticationSessionsChangeEvent }> = this._register(new Emitter<{ providerId: string, event: AuthenticationSessionsChangeEvent }>()); + readonly onDidChangeSessions: Event<{ providerId: string, event: AuthenticationSessionsChangeEvent }> = this._onDidChangeSessions.event; constructor() { super(); + this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + command: { + id: 'noAuthenticationProviders', + title: nls.localize('noAuthenticationProviders', "No authentication providers registered") + }, + }); } registerAuthenticationProvider(id: string, authenticationProvider: MainThreadAuthenticationProvider): void { this._authenticationProviders.set(id, authenticationProvider); this._onDidRegisterAuthenticationProvider.fire(id); + + if (authenticationProvider.dependents.length && this._placeholderMenuItem) { + this._placeholderMenuItem.dispose(); + this._placeholderMenuItem = undefined; + } } unregisterAuthenticationProvider(id: string): void { - this._authenticationProviders.delete(id); - this._onDidUnregisterAuthenticationProvider.fire(id); + const provider = this._authenticationProviders.get(id); + if (provider) { + provider.dispose(); + this._authenticationProviders.delete(id); + this._onDidUnregisterAuthenticationProvider.fire(id); + } + + if (!this._authenticationProviders.size) { + this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + command: { + id: 'noAuthenticationProviders', + title: nls.localize('noAuthenticationProviders', "No authentication providers registered") + }, + }); + } } - sessionsUpdate(id: string): void { - this._onDidChangeSessions.fire(id); + sessionsUpdate(id: string, event: AuthenticationSessionsChangeEvent): void { + this._onDidChangeSessions.fire({ providerId: id, event: event }); + const provider = this._authenticationProviders.get(id); + if (provider) { + provider.updateSessionItems(); + } } getDisplayName(id: string): string { diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts index acc8ba471c..eed26c32ac 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts @@ -28,7 +28,7 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerServ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { EditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack'; +import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack'; type ValidationResult = { canApply: true } | { canApply: false, reason: URI }; @@ -234,7 +234,7 @@ class BulkEditModel implements IDisposable { const multiModelEditStackElement = new MultiModelEditStackElement( this._label || localize('workspaceEdit', "Workspace Edit"), - tasks.map(t => new EditStackElement(t.model, t.getBeforeCursorState())) + tasks.map(t => new SingleModelEditStackElement(t.model, t.getBeforeCursorState())) ); this._undoRedoService.pushElement(multiModelEditStackElement); diff --git a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts index 6d72472eac..ccd8fbe541 100644 --- a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts +++ b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts @@ -19,6 +19,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { Schemas } from 'vs/base/common/network'; export class PreferencesEditorInput extends SideBySideEditorInput { static readonly ID: string = 'workbench.editorinputs.preferencesEditorInput'; @@ -105,7 +106,7 @@ export class SettingsEditor2Input extends EditorInput { private readonly _settingsModel: Settings2EditorModel; readonly resource: URI = URI.from({ - scheme: 'vscode-settings', + scheme: Schemas.vscodeSettings, path: `settingseditor` }); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService.ts deleted file mode 100644 index af79f80499..0000000000 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService.ts +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IResourceRefHandle, IUserDataSyncBackupStoreService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; -import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; - -export class UserDataSyncBackupStoreService implements IUserDataSyncBackupStoreService { - - _serviceBrand: undefined; - private readonly channel: IChannel; - - constructor( - @ISharedProcessService sharedProcessService: ISharedProcessService, - ) { - this.channel = sharedProcessService.getChannel('userDataSyncBackupStoreService'); - } - - backup(key: SyncResource, content: string): Promise { - return this.channel.call('backup', [key, content]); - } - - - getAllRefs(key: SyncResource): Promise { - return this.channel.call('getAllRefs', [key]); - } - - resolveContent(key: SyncResource, ref: string): Promise { - return this.channel.call('resolveContent', [key, ref]); - } - -} - -registerSingleton(IUserDataSyncBackupStoreService, UserDataSyncBackupStoreService); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts index a446767c1f..6bdf65882b 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, SyncResource, IUserDataSyncService, UserDataSyncError, SyncResourceConflicts } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, SyncResource, IUserDataSyncService, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; @@ -73,8 +73,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.channel.call('sync'); } - acceptConflict(conflict: URI, content: string): Promise { - return this.channel.call('acceptConflict', [conflict, content]); + stop(): Promise { + return this.channel.call('stop'); } reset(): Promise { @@ -85,16 +85,31 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.channel.call('resetLocal'); } - stop(): Promise { - return this.channel.call('stop'); + isFirstTimeSyncWithMerge(): Promise { + return this.channel.call('isFirstTimeSyncWithMerge'); + } + + acceptConflict(conflict: URI, content: string): Promise { + return this.channel.call('acceptConflict', [conflict, content]); } resolveContent(resource: URI): Promise { return this.channel.call('resolveContent', [resource]); } - isFirstTimeSyncWithMerge(): Promise { - return this.channel.call('isFirstTimeSyncWithMerge'); + async getLocalSyncResourceHandles(resource: SyncResource): Promise { + const handles = await this.channel.call('getLocalSyncResourceHandles', [resource]); + return handles.map(({ created, uri }) => ({ created, uri: URI.revive(uri) })); + } + + async getRemoteSyncResourceHandles(resource: SyncResource): Promise { + const handles = await this.channel.call('getRemoteSyncResourceHandles', [resource]); + return handles.map(({ created, uri }) => ({ created, uri: URI.revive(uri) })); + } + + async getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { + const result = await this.channel.call<{ resource: URI, comparableResource?: URI }[]>('getAssociatedResources', [resource, syncResourceHandle]); + return result.map(({ resource, comparableResource }) => ({ resource: URI.revive(resource), comparableResource: URI.revive(comparableResource) })); } private async updateStatus(status: SyncStatus): Promise { diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService.ts deleted file mode 100644 index de7df39fc7..0000000000 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService.ts +++ /dev/null @@ -1,58 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { SyncResource, IUserDataSyncStoreService, IUserDataSyncStore, getUserDataSyncStore, IUserData, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; -import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IProductService } from 'vs/platform/product/common/productService'; - -export class UserDataSyncStoreService implements IUserDataSyncStoreService { - - _serviceBrand: undefined; - private readonly channel: IChannel; - readonly userDataSyncStore: IUserDataSyncStore | undefined; - - constructor( - @ISharedProcessService sharedProcessService: ISharedProcessService, - @IProductService productService: IProductService, - @IConfigurationService configurationService: IConfigurationService - ) { - this.channel = sharedProcessService.getChannel('userDataSyncStoreService'); - this.userDataSyncStore = getUserDataSyncStore(productService, configurationService); - } - - read(key: SyncResource, oldValue: IUserData | null, source?: SyncResource): Promise { - throw new Error('Not Supported'); - } - - write(key: SyncResource, content: string, ref: string | null, source?: SyncResource): Promise { - throw new Error('Not Supported'); - } - - manifest(): Promise { - throw new Error('Not Supported'); - } - - clear(): Promise { - throw new Error('Not Supported'); - } - - getAllRefs(key: SyncResource): Promise { - return this.channel.call('getAllRefs', [key]); - } - - resolveContent(key: SyncResource, ref: string): Promise { - return this.channel.call('resolveContent', [key, ref]); - } - - delete(key: SyncResource): Promise { - return this.channel.call('delete', [key]); - } - -} - -registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index c1ead31d54..9948c63f76 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -28,12 +28,12 @@ export const enum WorkingCopyCapabilities { * `IBackupFileService.resolve(workingCopy.resource)` to * retrieve the backup when loading the working copy. */ -export interface IWorkingCopyBackup { +export interface IWorkingCopyBackup { /** * Any serializable metadata to be associated with the backup. */ - meta?: object; + meta?: MetaType; /** * Use this for larger textual content of the backup. diff --git a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts index 16967ee12e..a96eeea336 100644 --- a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts @@ -51,7 +51,7 @@ suite('MainThreadDocumentsAndEditors', () => { const dialogService = new TestDialogService(); const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); - modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService); + modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService, dialogService); codeEditorService = new TestCodeEditorService(); textFileService = new class extends mock() { isDirty() { return false; } diff --git a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts index adec2a43b3..1fbfff9eb7 100644 --- a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts @@ -73,7 +73,7 @@ suite('MainThreadEditors', () => { const dialogService = new TestDialogService(); const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); - modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService); + modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService, dialogService); const services = new ServiceCollection(); diff --git a/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts index ad05abdf3e..ed52acb0c9 100644 --- a/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts @@ -89,7 +89,7 @@ suite.skip('QuickOpen performance (integration)', () => { [IDialogService, dialogService], [INotificationService, notificationService], [IUndoRedoService, undoRedoService], - [IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), new NullLogService(), undoRedoService)], + [IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), new NullLogService(), undoRedoService, dialogService)], [IWorkspaceContextService, new TestContextService(testWorkspace(URI.file(testWorkspacePath)))], [IEditorService, new TestEditorService()], [IEditorGroupsService, new TestEditorGroupsService()], diff --git a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts index e5bc17281d..642bfc7f8d 100644 --- a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts @@ -79,7 +79,7 @@ suite.skip('TextSearch performance (integration)', () => { [IDialogService, dialogService], [INotificationService, notificationService], [IUndoRedoService, undoRedoService], - [IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), logService, undoRedoService)], + [IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), logService, undoRedoService, dialogService)], [IWorkspaceContextService, new TestContextService(testWorkspace(URI.file(testWorkspacePath)))], [IEditorService, new TestEditorService()], [IEditorGroupsService, new TestEditorGroupsService()], diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 9ed7b443e7..7d65987ad1 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -207,6 +207,7 @@ export class TestElectronService implements IElectronService { async relaunch(options?: { addArgs?: string[] | undefined; removeArgs?: string[] | undefined; } | undefined): Promise { } async reload(): Promise { } async closeWindow(): Promise { } + async closeWindowById(): Promise { } async quit(): Promise { } async openDevTools(options?: Electron.OpenDevToolsOptions | undefined): Promise { } async toggleDevTools(): Promise { } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 27628bc5f3..bc5a18bcb4 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -282,7 +282,6 @@ import 'vs/workbench/contrib/scm/browser/scmViewlet'; /* {{SQL CARBON EDIT}} // Debug import 'vs/workbench/contrib/debug/browser/debug.contribution'; -import 'vs/workbench/contrib/debug/browser/debugQuickOpen'; import 'vs/workbench/contrib/debug/browser/debugEditorContribution'; import 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; import 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; @@ -305,7 +304,6 @@ import 'vs/workbench/contrib/customEditor/browser/webviewEditor.contribution'; // Extensions Management import 'vs/workbench/contrib/extensions/browser/extensions.contribution'; -import 'vs/workbench/contrib/extensions/browser/extensionsQuickOpen'; import 'vs/workbench/contrib/extensions/browser/extensionsViewlet'; // Output View @@ -314,7 +312,6 @@ import 'vs/workbench/contrib/output/browser/outputView'; // Terminal import 'vs/workbench/contrib/terminal/browser/terminal.contribution'; -import 'vs/workbench/contrib/terminal/browser/terminalQuickOpen'; import 'vs/workbench/contrib/terminal/browser/terminalView'; // Relauncher diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index deba2733c0..0eacc56989 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -51,8 +51,6 @@ import 'vs/workbench/services/workspaces/electron-browser/workspacesService'; import 'vs/workbench/services/workspaces/electron-browser/workspaceEditingService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService'; -import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService'; -import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService'; import 'vs/workbench/services/authentication/electron-browser/authenticationTokenService'; import 'vs/workbench/services/authentication/browser/authenticationService'; import 'vs/workbench/services/host/electron-browser/desktopHostService'; diff --git a/yarn.lock b/yarn.lock index ac7644be2e..7585b1b15b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9899,10 +9899,10 @@ vsce@1.48.0: yauzl "^2.3.1" yazl "^2.2.2" -vscode-debugprotocol@1.39.0: - version "1.39.0" - resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.39.0.tgz#0c639178d0d5ea7de7903b6478b53d2bc0d77461" - integrity sha512-Wkvgtuz90vjtQBcvw9Z+BYa4dA6W+sHwHMpqvJVNmwWSuT3JZdl0XDhZNLqtMXkVF4okxtAe0MmbupPSt+gnAQ== +vscode-debugprotocol@1.40.0-pre.1: + version "1.40.0-pre.1" + resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.40.0-pre.1.tgz#62c066c0520cc5e318dfc9873907574018bc8460" + integrity sha512-MLlNUSoJbRPNP/7PpnNLOOubZP/X0ObDEjwC6frrn/GR+bT943S1iIdj9aMjT7i93HSpsIAx8YbwkqId7nxLgw== vscode-minimist@^1.2.2: version "1.2.2"