diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index c2755a8478bc..eeacf8c78bcd 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -46,7 +46,7 @@ }, { "name": "ms-vscode.js-debug-nightly", - "version": "2020.2.1117", + "version": "2020.2.1317", "forQualities": [ "insider" ], diff --git a/extensions/git/package.json b/extensions/git/package.json index c1cd8b5135c0..dcfdcd2171c7 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -447,6 +447,22 @@ "command": "git.stashDrop", "title": "%command.stashDrop%", "category": "Git" + }, + { + "command": "git.timeline.openDiff", + "title": "%command.timelineOpenDiff%", + "icon": "$(compare-changes)", + "category": "Git" + }, + { + "command": "git.timeline.copyCommitId", + "title": "%command.timelineCopyCommitId%", + "category": "Git" + }, + { + "command": "git.timeline.copyCommitMessage", + "title": "%command.timelineCopyCommitMessage%", + "category": "Git" } ], "menus": { @@ -718,14 +734,21 @@ { "command": "git.stashDrop", "when": "config.git.enabled && gitOpenRepositoryCount != 0" + }, + { + "command": "git.timeline.openDiff", + "when": "false" + }, + { + "command": "git.timeline.copyCommitId", + "when": "false" + }, + { + "command": "git.timeline.copyCommitMessage", + "when": "false" } ], "scm/title": [ - { - "command": "git.init", - "group": "navigation", - "when": "config.git.enabled && !scmProvider && gitOpenRepositoryCount == 0 && workspaceFolderCount != 0" - }, { "command": "git.commit", "group": "navigation", @@ -1248,6 +1271,28 @@ "command": "git.revertChange", "when": "originalResourceScheme == git" } + ], + "timeline/item/context": [ + { + "command": "git.timeline.openDiff", + "group": "inline", + "when": "timelineItem =~ /git:file\\b/" + }, + { + "command": "git.timeline.openDiff", + "group": "1_timeline", + "when": "timelineItem =~ /git:file\\b/" + }, + { + "command": "git.timeline.copyCommitId", + "group": "2_timeline@1", + "when": "timelineItem =~ /git:file:commit\\b/" + }, + { + "command": "git.timeline.copyCommitMessage", + "group": "2_timeline@2", + "when": "timelineItem =~ /git:file:commit\\b/" + } ] }, "configuration": { @@ -1761,7 +1806,34 @@ 72 ] } - } + }, + "viewsWelcome": [ + { + "view": "workbench.scm", + "contents": "%view.workbench.scm.disabled%", + "when": "!config.git.enabled" + }, + { + "view": "workbench.scm", + "contents": "%view.workbench.scm.missing%", + "when": "config.git.enabled && git.missing" + }, + { + "view": "workbench.scm", + "contents": "%view.workbench.scm.empty%", + "when": "config.git.enabled && !git.missing && workbenchState == empty" + }, + { + "view": "workbench.scm", + "contents": "%view.workbench.scm.folder%", + "when": "config.git.enabled && !git.missing && workbenchState == folder" + }, + { + "view": "workbench.scm", + "contents": "%view.workbench.scm.workspace%", + "when": "config.git.enabled && !git.missing && workbenchState == workspace" + } + ] }, "dependencies": { "byline": "^5.0.0", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index f3d755196534..53325c0f5f70 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -70,6 +70,9 @@ "command.stashApply": "Apply Stash...", "command.stashApplyLatest": "Apply Latest Stash", "command.stashDrop": "Drop Stash...", + "command.timelineOpenDiff": "Open Changes", + "command.timelineCopyCommitId": "Copy Commit ID", + "command.timelineCopyCommitMessage": "Copy Commit Message", "config.enabled": "Whether git is enabled.", "config.path": "Path and filename of the git executable, e.g. `C:\\Program Files\\Git\\bin\\git.exe` (Windows).", "config.autoRepositoryDetection": "Configures when repositories should be automatically detected.", @@ -127,7 +130,7 @@ "config.showProgress": "Controls whether git actions should show progress.", "config.rebaseWhenSync": "Force git to use rebase when running the sync command.", "config.confirmEmptyCommits": "Always confirm the creation of empty commits for the 'Git: Commit Empty' command.", - "config.fetchOnPull": "Fetch all branches when pulling or just the current one.", + "config.fetchOnPull": "When enabled, fetch all branches when pulling. Otherwise, fetch just the current one.", "config.pullTags": "Fetch all tags when pulling.", "config.autoStash": "Stash any changes before pulling and restore them after successful pull.", "config.allowForcePush": "Controls whether force push (with or without lease) is enabled.", @@ -146,5 +149,10 @@ "colors.untracked": "Color for untracked resources.", "colors.ignored": "Color for ignored resources.", "colors.conflict": "Color for resources with conflicts.", - "colors.submodule": "Color for submodule resources." + "colors.submodule": "Color for submodule resources.", + "view.workbench.scm.missing": "A valid git installation was not detected, more details can be found in the [git output](command:git.showOutput).\nPlease [install git](https://git-scm.com/), or learn more about how to use Git and source control in VS Code in [our docs](https://aka.ms/vscode-scm).\nIf you're using a different version control system, you can [search the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22) for additional extensions.", + "view.workbench.scm.disabled": "If you would like to use git features, please enable git in your [settings](command:workbench.action.openSettings?%5B%22git.enabled%22%5D).\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.empty": "In order to use git features, you can open a folder containing a git repository or clone from a URL.\n[Open Folder](command:vscode.openFolder)\n[Clone from URL](command:git.clone)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.folder": "The folder currently open doesn't have a git repository.\n[Initialize Repository](command:git.init)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.workspace": "The workspace currently open doesn't have any folders containing git repositories.\n[Initialize Repository](command:git.init)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm)." } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index dba21595ea8a..9d8fe2a4ef46 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -6,7 +6,7 @@ import { lstat, Stats } from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder } from 'vscode'; +import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env } from 'vscode'; import TelemetryReporter from 'vscode-extension-telemetry'; import * as nls from 'vscode-nls'; import { Branch, GitErrorCodes, Ref, RefType, Status, CommitOptions } from './api/git'; @@ -17,6 +17,7 @@ import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineC import { fromGitUri, toGitUri, isGitUri } from './uri'; import { grep, isDescendant, pathEquals } from './util'; import { Log, LogLevel } from './log'; +import { GitTimelineItem } from './timelineProvider'; const localize = nls.loadMessageBundle(); @@ -2331,23 +2332,47 @@ export class CommandCenter { return result && result.stash; } - @command('git.openDiff', { repository: false }) - async openDiff(uri: Uri, lhs: string, rhs: string) { + @command('git.timeline.openDiff', { repository: false }) + async timelineOpenDiff(item: TimelineItem, uri: Uri | undefined, _source: string) { + // eslint-disable-next-line eqeqeq + if (uri == null || !GitTimelineItem.is(item)) { + return undefined; + } + const basename = path.basename(uri.fsPath); let title; - if ((lhs === 'HEAD' || lhs === '~') && rhs === '') { + if ((item.previousRef === 'HEAD' || item.previousRef === '~') && item.ref === '') { title = `${basename} (Working Tree)`; } - else if (lhs === 'HEAD' && rhs === '~') { + else if (item.previousRef === 'HEAD' && item.ref === '~') { title = `${basename} (Index)`; } else { - title = `${basename} (${lhs.endsWith('^') ? `${lhs.substr(0, 8)}^` : lhs.substr(0, 8)}) \u27f7 ${basename} (${rhs.endsWith('^') ? `${rhs.substr(0, 8)}^` : rhs.substr(0, 8)})`; + title = `${basename} (${item.shortPreviousRef}) \u27f7 ${basename} (${item.shortRef})`; } - return commands.executeCommand('vscode.diff', toGitUri(uri, lhs), rhs === '' ? uri : toGitUri(uri, rhs), title); + return commands.executeCommand('vscode.diff', toGitUri(uri, item.previousRef), item.ref === '' ? uri : toGitUri(uri, item.ref), title); } + @command('git.timeline.copyCommitId', { repository: false }) + async timelineCopyCommitId(item: TimelineItem, _uri: Uri | undefined, _source: string) { + if (!GitTimelineItem.is(item)) { + return; + } + + env.clipboard.writeText(item.ref); + } + + @command('git.timeline.copyCommitMessage', { repository: false }) + async timelineCopyCommitMessage(item: TimelineItem, _uri: Uri | undefined, _source: string) { + if (!GitTimelineItem.is(item)) { + return; + } + + env.clipboard.writeText(item.message); + } + + private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any { const result = (...args: any[]) => { let result: Promise; diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 7bb5081d12f3..ddf16d76aacb 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -175,6 +175,7 @@ export async function activate(context: ExtensionContext): Promise console.warn(err.message); outputChannel.appendLine(err.message); + commands.executeCommand('setContext', 'git.missing', true); warnAboutMissingGit(); return new GitExtensionImpl(); diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 3aa36a0dccbe..9db3495aec73 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -5,19 +5,62 @@ import * as dayjs from 'dayjs'; import * as advancedFormat from 'dayjs/plugin/advancedFormat'; -import * as relativeTime from 'dayjs/plugin/relativeTime'; -import { CancellationToken, Disposable, Event, EventEmitter, ThemeIcon, TimelineItem, TimelineProvider, Uri, workspace, TimelineChangeEvent } from 'vscode'; +import { CancellationToken, Disposable, Event, EventEmitter, ThemeIcon, Timeline, TimelineChangeEvent, TimelineCursor, TimelineItem, TimelineProvider, Uri, workspace } from 'vscode'; import { Model } from './model'; import { Repository } from './repository'; import { debounce } from './decorators'; import { Status } from './api/git'; dayjs.extend(advancedFormat); -dayjs.extend(relativeTime); // TODO[ECA]: Localize all the strings // TODO[ECA]: Localize or use a setting for date format +export class GitTimelineItem extends TimelineItem { + static is(item: TimelineItem): item is GitTimelineItem { + return item instanceof GitTimelineItem; + } + + readonly ref: string; + readonly previousRef: string; + readonly message: string; + + constructor( + ref: string, + previousRef: string, + message: string, + timestamp: number, + id: string, + contextValue: string + ) { + const index = message.indexOf('\n'); + const label = index !== -1 ? `${message.substring(0, index)} \u2026` : message; + + super(label, timestamp); + + this.ref = ref; + this.previousRef = previousRef; + this.message = message; + this.id = id; + this.contextValue = contextValue; + } + + get shortRef() { + return this.shortenRef(this.ref); + } + + get shortPreviousRef() { + return this.shortenRef(this.previousRef); + } + + private shortenRef(ref: string): string { + if (ref === '' || ref === '~' || ref === 'HEAD') { + return ref; + } + return ref.endsWith('^') ? `${ref.substr(0, 8)}^` : ref.substr(0, 8); + } +} + export class GitTimelineProvider implements TimelineProvider { private _onDidChange = new EventEmitter(); get onDidChange(): Event { @@ -44,7 +87,7 @@ export class GitTimelineProvider implements TimelineProvider { this._disposable.dispose(); } - async provideTimeline(uri: Uri, _token: CancellationToken): Promise { + async provideTimeline(uri: Uri, _cursor: TimelineCursor, _token: CancellationToken): Promise { // console.log(`GitTimelineProvider.provideTimeline: uri=${uri} state=${this._model.state}`); const repo = this._model.getRepository(uri); @@ -53,7 +96,7 @@ export class GitTimelineProvider implements TimelineProvider { this._repoStatusDate = undefined; this._repo = undefined; - return []; + return { items: [] }; } if (this._repo?.root !== repo.root) { @@ -72,25 +115,17 @@ export class GitTimelineProvider implements TimelineProvider { const commits = await repo.logFile(uri); let dateFormatter: dayjs.Dayjs; - const items = commits.map(c => { - let message = c.message; - - const index = message.indexOf('\n'); - if (index !== -1) { - message = `${message.substring(0, index)} \u2026`; - } - + const items = commits.map(c => { dateFormatter = dayjs(c.authorDate); - const item = new TimelineItem(message, c.authorDate?.getTime() ?? 0); - item.id = c.hash; + const item = new GitTimelineItem(c.hash, `${c.hash}^`, c.message, c.authorDate?.getTime() ?? 0, c.hash, 'git:file:commit'); item.iconPath = new (ThemeIcon as any)('git-commit'); - item.description = `${dateFormatter.fromNow()} \u2022 ${c.authorName}`; - item.detail = `${c.authorName} (${c.authorEmail}) \u2014 ${c.hash.substr(0, 8)}\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n\n${c.message}`; + item.description = c.authorName; + item.detail = `${c.authorName} (${c.authorEmail}) \u2014 ${c.hash.substr(0, 8)}\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n\n${c.message}`; item.command = { - title: 'Open Diff', - command: 'git.openDiff', - arguments: [uri, `${c.hash}^`, c.hash] + title: 'Open Comparison', + command: 'git.timeline.openDiff', + arguments: [uri, this.id, item] }; return item; @@ -123,16 +158,15 @@ export class GitTimelineProvider implements TimelineProvider { break; } - const item = new TimelineItem('Staged Changes', date.getTime()); - item.id = 'index'; + const item = new GitTimelineItem('~', 'HEAD', 'Staged Changes', date.getTime(), 'index', 'git:file:index'); // TODO[ECA]: Replace with a better icon -- reflecting its status maybe? item.iconPath = new (ThemeIcon as any)('git-commit'); - item.description = `${dateFormatter.fromNow()} \u2022 You`; - item.detail = `You \u2014 Index\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n${status}`; + item.description = 'You'; + item.detail = `You \u2014 Index\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n${status}`; item.command = { title: 'Open Comparison', - command: 'git.openDiff', - arguments: [uri, 'HEAD', '~'] + command: 'git.timeline.openDiff', + arguments: [uri, this.id, item] }; items.push(item); @@ -166,22 +200,21 @@ export class GitTimelineProvider implements TimelineProvider { break; } - const item = new TimelineItem('Uncommited Changes', date.getTime()); - item.id = 'working'; + const item = new GitTimelineItem('', index ? '~' : 'HEAD', 'Uncommited Changes', date.getTime(), 'working', 'git:file:working'); // TODO[ECA]: Replace with a better icon -- reflecting its status maybe? item.iconPath = new (ThemeIcon as any)('git-commit'); - item.description = `${dateFormatter.fromNow()} \u2022 You`; - item.detail = `You \u2014 Working Tree\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n${status}`; + item.description = 'You'; + item.detail = `You \u2014 Working Tree\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n${status}`; item.command = { title: 'Open Comparison', - command: 'git.openDiff', - arguments: [uri, index ? '~' : 'HEAD', ''] + command: 'git.timeline.openDiff', + arguments: [uri, this.id, item] }; items.push(item); } - return items; + return { items: items }; } private onRepositoriesChanged(_repo: Repository) { @@ -208,6 +241,6 @@ export class GitTimelineProvider implements TimelineProvider { @debounce(500) private fireChanged() { - this._onDidChange.fire(); + this._onDidChange.fire({}); } } diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index af5b10127dc4..ac135e43a804 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -382,7 +382,7 @@ har-schema@^2.0.0: resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= -har-validator@~5.1.0: +har-validator@~5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== @@ -697,17 +697,12 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -psl@^1.1.24: +psl@^1.1.28: version "1.7.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== @@ -723,9 +718,9 @@ querystringify@^2.1.1: integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== request@^2.88.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -734,7 +729,7 @@ request@^2.88.0: extend "~3.0.2" forever-agent "~0.6.1" form-data "~2.3.2" - har-validator "~5.1.0" + har-validator "~5.1.3" http-signature "~1.2.0" is-typedarray "~1.0.0" isstream "~0.1.2" @@ -744,7 +739,7 @@ request@^2.88.0: performance-now "^2.1.0" qs "~6.5.2" safe-buffer "^5.1.2" - tough-cookie "~2.4.3" + tough-cookie "~2.5.0" tunnel-agent "^0.6.0" uuid "^3.3.2" @@ -822,13 +817,13 @@ supports-color@5.4.0: dependencies: has-flag "^3.0.0" -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== dependencies: - psl "^1.1.24" - punycode "^1.4.1" + psl "^1.1.28" + punycode "^2.1.1" tunnel-agent@^0.6.0: version "0.6.0" diff --git a/extensions/typescript-language-features/src/features/referencesCodeLens.ts b/extensions/typescript-language-features/src/features/referencesCodeLens.ts index 5e92ce06e01a..7aa228f8f8e4 100644 --- a/extensions/typescript-language-features/src/features/referencesCodeLens.ts +++ b/extensions/typescript-language-features/src/features/referencesCodeLens.ts @@ -99,6 +99,14 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens case PConst.Kind.memberSetAccessor: case PConst.Kind.constructorImplementation: case PConst.Kind.memberVariable: + // Don't show if child and parent have same start + // For https://github.com/microsoft/vscode/issues/90396 + if (parent && + typeConverters.Position.fromLocation(parent.spans[0].start).isEqual(typeConverters.Position.fromLocation(item.spans[0].start)) + ) { + return null; + } + // Only show if parent is a class type object (not a literal) switch (parent?.kind) { case PConst.Kind.class: diff --git a/extensions/typescript-language-features/src/test/referencesCodeLens.test.ts b/extensions/typescript-language-features/src/test/referencesCodeLens.test.ts index 7ca35ed89ccb..89689d57ae68 100644 --- a/extensions/typescript-language-features/src/test/referencesCodeLens.test.ts +++ b/extensions/typescript-language-features/src/test/referencesCodeLens.test.ts @@ -91,6 +91,19 @@ suite('TypeScript References', () => { const codeLenses = await getCodeLenses(testDocumentUri); assert.strictEqual(codeLenses?.length, 0); }); + + test('Should not show duplicate references on ES5 class (https://github.com/microsoft/vscode/issues/90396)', async () => { + const testDocumentUri = vscode.Uri.parse('untitled:test3.js'); + await createTestEditor(testDocumentUri, + `function A() {`, + ` console.log("hi");`, + `}`, + `A.x = {};`, + ); + + const codeLenses = await getCodeLenses(testDocumentUri); + assert.strictEqual(codeLenses?.length, 1); + }); }); function getCodeLenses(document: vscode.Uri): Thenable { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts index e785f1d4afbe..06d284c37622 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'mocha'; import * as assert from 'assert'; -import * as vscode from 'vscode'; +import 'mocha'; +import * as os from 'os'; import { join } from 'path'; -import { closeAllEditors, disposeAll, conditionalTest } from '../utils'; +import * as vscode from 'vscode'; +import { closeAllEditors, conditionalTest, delay, disposeAll } from '../utils'; const webviewId = 'myWebview'; @@ -332,8 +333,30 @@ suite('Webview tests', () => { webview.webview.postMessage({ value: 1 }); await firstResponse; assert.strictEqual(webview.viewColumn, vscode.ViewColumn.One); - }); + + if (os.platform() === 'darwin') { + conditionalTest('webview can copy text from webview', async () => { + const expectedText = `webview text from: ${Date.now()}!`; + + const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true })); + const ready = getMesssage(webview); + + + webview.webview.html = createHtmlDocumentWithBody(/*html*/` + ${expectedText} + `); + await ready; + + await vscode.commands.executeCommand('editor.action.webvieweditor.copy'); + await delay(200); // Make sure copy has time to reach webview + assert.strictEqual(await vscode.env.clipboard.readText(), expectedText); + }); + } }); function createHtmlDocumentWithBody(body: string): string { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index a8fef8ec2adc..620ce762632e 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { workspace, window, commands, ViewColumn, TextEditorViewColumnChangeEvent, Uri, Selection, Position, CancellationTokenSource, TextEditorSelectionChangeKind, env, UIKind } from 'vscode'; +import { workspace, window, commands, ViewColumn, TextEditorViewColumnChangeEvent, Uri, Selection, Position, CancellationTokenSource, TextEditorSelectionChangeKind } from 'vscode'; import { join } from 'path'; import { closeAllEditors, pathEquals, createRandomFile } from '../utils'; @@ -146,12 +146,6 @@ suite('window namespace tests', () => { }); test('active editor not always correct... #49125', async function () { - if (env.uiKind === UIKind.Web) { - // https://github.com/microsoft/vscode/issues/90470 - // https://github.com/microsoft/playwright/issues/979 - this.skip(); - return; - } const randomFile1 = await createRandomFile(); const randomFile2 = await createRandomFile(); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 25b34c5827b7..c26c88671ddf 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -215,13 +215,6 @@ suite('workspace-namespace', () => { }); test('eol, change via onWillSave', async function () { - if (vscode.env.uiKind === vscode.UIKind.Web) { - // TODO@Jo Test seems to fail when running in web due to - // onWillSaveTextDocument not getting called - this.skip(); - return; - } - let called = false; let sub = vscode.workspace.onWillSaveTextDocument(e => { called = true; @@ -245,26 +238,32 @@ suite('workspace-namespace', () => { sub.dispose(); }); + function assertEqualPath(a: string, b: string): void { + assert.ok(pathEquals(a, b), `${a} <-> ${b}`); + } + test('events: onDidOpenTextDocument, onDidChangeTextDocument, onDidSaveTextDocument', async () => { const file = await createRandomFile(); let disposables: vscode.Disposable[] = []; + await vscode.workspace.saveAll(); + let pendingAsserts: Function[] = []; let onDidOpenTextDocument = false; disposables.push(vscode.workspace.onDidOpenTextDocument(e => { - pendingAsserts.push(() => assert.ok(pathEquals(e.uri.fsPath, file.fsPath))); + pendingAsserts.push(() => assertEqualPath(e.uri.fsPath, file.fsPath)); onDidOpenTextDocument = true; })); let onDidChangeTextDocument = false; disposables.push(vscode.workspace.onDidChangeTextDocument(e => { - pendingAsserts.push(() => assert.ok(pathEquals(e.document.uri.fsPath, file.fsPath))); + pendingAsserts.push(() => assertEqualPath(e.document.uri.fsPath, file.fsPath)); onDidChangeTextDocument = true; })); let onDidSaveTextDocument = false; disposables.push(vscode.workspace.onDidSaveTextDocument(e => { - pendingAsserts.push(() => assert.ok(pathEquals(e.uri.fsPath, file.fsPath))); + pendingAsserts.push(() => assertEqualPath(e.uri.fsPath, file.fsPath)); onDidSaveTextDocument = true; })); @@ -291,7 +290,7 @@ suite('workspace-namespace', () => { let onDidSaveTextDocument = false; disposables.push(vscode.workspace.onDidSaveTextDocument(e => { - pendingAsserts.push(() => assert.ok(pathEquals(e.uri.fsPath, file.fsPath))); + pendingAsserts.push(() => assertEqualPath(e.uri.fsPath, file.fsPath)); onDidSaveTextDocument = true; })); diff --git a/package.json b/package.json index 531ea49e514a..2712fd40e09b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.43.0", - "distro": "a87981a5cc52fed24735d447e954adccb9c9f914", + "distro": "821d61f0e38d2eb3e02bec3962fe61e4e5a61527", "author": { "name": "Microsoft Corporation" }, @@ -139,7 +139,7 @@ "opn": "^6.0.0", "optimist": "0.3.5", "p-all": "^1.0.0", - "playwright": "^0.10.0", + "playwright": "0.11.0", "pump": "^1.0.1", "queue": "3.0.6", "rcedit": "^1.1.0", diff --git a/scripts/code.sh b/scripts/code.sh index 4ba1a00b9f7c..0afe96bfcb6a 100755 --- a/scripts/code.sh +++ b/scripts/code.sh @@ -50,7 +50,7 @@ function code() { export VSCODE_LOGS= # Launch Code - exec "$CODE" . "$@" + exec "$CODE" . --no-sandbox "$@" } function code-wsl() diff --git a/src/main.js b/src/main.js index 7242b0cb156a..6a6cb060acc1 100644 --- a/src/main.js +++ b/src/main.js @@ -82,7 +82,7 @@ app.once('ready', function () { traceOptions: args['trace-options'] || 'record-until-full,enable-sampling' }; - contentTracing.startRecording(traceOptions, () => onReady()); + contentTracing.startRecording(traceOptions).finally(() => onReady()); } else { onReady(); } @@ -133,6 +133,9 @@ function configureCommandlineSwitchesSync(cliArgs) { // provided by Electron 'disable-color-correct-rendering' ]; + if (process.platform === 'linux') { + SUPPORTED_ELECTRON_SWITCHES.push('force-renderer-accessibility'); + } // Read argv config const argvConfig = readArgvConfigSync(); @@ -159,6 +162,9 @@ function configureCommandlineSwitchesSync(cliArgs) { app.commandLine.appendSwitch('js-flags', jsFlags); } + // TODO@Ben TODO@Deepak Electron 7 workaround for https://github.com/microsoft/vscode/issues/88873 + app.commandLine.appendSwitch('disable-features', 'LayoutNG'); + return argvConfig; } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css index e60151c5aa3c..2fa65d719233 100644 --- a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css +++ b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css @@ -5,7 +5,7 @@ @font-face { font-family: "codicon"; - src: url("./codicon.ttf?6caeeccc06315e827f3bff83885456fb") format("truetype"); + src: url("./codicon.ttf?d0510f6ecacbb2788db2b3162273a3d8") format("truetype"); } .codicon[class*='codicon-'] { diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf index a62bc6bd7440..f712f5cd0531 100644 Binary files a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf and b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf differ diff --git a/src/vs/base/common/date.ts b/src/vs/base/common/date.ts index a6da9d5ae336..acfc19d6edb3 100644 --- a/src/vs/base/common/date.ts +++ b/src/vs/base/common/date.ts @@ -5,6 +5,53 @@ import { pad } from './strings'; +const minute = 60; +const hour = minute * 60; +const day = hour * 24; +const week = day * 7; +const month = day * 30; +const year = day * 365; + +// TODO[ECA]: Localize strings +export function fromNow(date: number | Date) { + if (typeof date !== 'number') { + date = date.getTime(); + } + + const seconds = Math.round((new Date().getTime() - date) / 1000); + if (seconds < 30) { + return 'now'; + } + + let value: number; + let unit: string; + if (seconds < minute) { + value = seconds; + unit = 'sec'; + } else if (seconds < hour) { + value = Math.floor(seconds / minute); + unit = 'min'; + } else if (seconds < day) { + value = Math.floor(seconds / hour); + unit = 'hr'; + } else if (seconds < week) { + value = Math.floor(seconds / day); + unit = 'day'; + } else if (seconds < month) { + value = Math.floor(seconds / week); + unit = 'wk'; + } else if (seconds < year) { + value = Math.floor(seconds / month); + unit = 'mo'; + } else { + value = Math.floor(seconds / year); + unit = 'yr'; + } + + return `${value} ${unit}${value === 1 ? '' : 's'}`; + +} + export function toLocalISOString(date: Date): string { return date.getFullYear() + '-' + pad(date.getMonth() + 1, 2) + diff --git a/src/vs/base/browser/linkedText.ts b/src/vs/base/common/linkedText.ts similarity index 79% rename from src/vs/base/browser/linkedText.ts rename to src/vs/base/common/linkedText.ts index 3d69948aa427..ee268d9fdcd2 100644 --- a/src/vs/base/browser/linkedText.ts +++ b/src/vs/base/common/linkedText.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { memoize } from 'vs/base/common/decorators'; + export interface ILink { readonly label: string; readonly href: string; @@ -10,7 +12,16 @@ export interface ILink { } export type LinkedTextNode = string | ILink; -export type LinkedText = LinkedTextNode[]; + +export class LinkedText { + + constructor(readonly nodes: LinkedTextNode[]) { } + + @memoize + toString(): string { + return this.nodes.map(node => typeof node === 'string' ? node : node.label).join(''); + } +} const LINK_REGEX = /\[([^\]]+)\]\(((?:https?:\/\/|command:)[^\)\s]+)(?: "([^"]+)")?\)/gi; @@ -40,5 +51,5 @@ export function parseLinkedText(text: string): LinkedText { result.push(text.substring(index)); } - return result; + return new LinkedText(result); } diff --git a/src/vs/base/test/browser/linkedText.test.ts b/src/vs/base/test/common/linkedText.test.ts similarity index 77% rename from src/vs/base/test/browser/linkedText.test.ts rename to src/vs/base/test/common/linkedText.test.ts index 2c765555ffe7..8e1cb887485d 100644 --- a/src/vs/base/test/browser/linkedText.test.ts +++ b/src/vs/base/test/common/linkedText.test.ts @@ -4,50 +4,50 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { parseLinkedText } from 'vs/base/browser/linkedText'; +import { parseLinkedText } from 'vs/base/common/linkedText'; suite('LinkedText', () => { test('parses correctly', () => { - assert.deepEqual(parseLinkedText(''), []); - assert.deepEqual(parseLinkedText('hello'), ['hello']); - assert.deepEqual(parseLinkedText('hello there'), ['hello there']); - assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href).'), [ + assert.deepEqual(parseLinkedText('').nodes, []); + assert.deepEqual(parseLinkedText('hello').nodes, ['hello']); + assert.deepEqual(parseLinkedText('hello there').nodes, ['hello there']); + assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href).').nodes, [ 'Some message with ', { label: 'link text', href: 'http://link.href' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href "and a title").'), [ + assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href "and a title").').nodes, [ 'Some message with ', { label: 'link text', href: 'http://link.href', title: 'and a title' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [link text](random stuff).'), [ + assert.deepEqual(parseLinkedText('Some message with [link text](random stuff).').nodes, [ 'Some message with [link text](random stuff).' ]); - assert.deepEqual(parseLinkedText('Some message with [https link](https://link.href).'), [ + assert.deepEqual(parseLinkedText('Some message with [https link](https://link.href).').nodes, [ 'Some message with ', { label: 'https link', href: 'https://link.href' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [https link](https:).'), [ + assert.deepEqual(parseLinkedText('Some message with [https link](https:).').nodes, [ 'Some message with [https link](https:).' ]); - assert.deepEqual(parseLinkedText('Some message with [a command](command:foobar).'), [ + assert.deepEqual(parseLinkedText('Some message with [a command](command:foobar).').nodes, [ 'Some message with ', { label: 'a command', href: 'command:foobar' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [a command](command:).'), [ + assert.deepEqual(parseLinkedText('Some message with [a command](command:).').nodes, [ 'Some message with [a command](command:).' ]); - assert.deepEqual(parseLinkedText('link [one](command:foo "nice") and link [two](http://foo)...'), [ + assert.deepEqual(parseLinkedText('link [one](command:foo "nice") and link [two](http://foo)...').nodes, [ 'link ', { label: 'one', href: 'command:foo', title: 'nice' }, ' and link ', { label: 'two', href: 'http://foo' }, '...' ]); - assert.deepEqual(parseLinkedText('link\n[one](command:foo "nice")\nand link [two](http://foo)...'), [ + assert.deepEqual(parseLinkedText('link\n[one](command:foo "nice")\nand link [two](http://foo)...').nodes, [ 'link\n', { label: 'one', href: 'command:foo', title: 'nice' }, '\nand link ', diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index d17a34fbf0c8..45f6f17ce064 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkbenchConstructionOptions, create, URI, Event, Emitter, UriComponents, ICredentialsProvider, IURLCallbackProvider, IWorkspaceProvider, IWorkspace, IApplicationLinkProvider, IApplicationLink } from 'vs/workbench/workbench.web.api'; +import { IWorkbenchConstructionOptions, create, URI, Event, Emitter, UriComponents, ICredentialsProvider, IURLCallbackProvider, IWorkspaceProvider, IWorkspace, IApplicationLink } from 'vs/workbench/workbench.web.api'; import { generateUuid } from 'vs/base/common/uuid'; import { CancellationToken } from 'vs/base/common/cancellation'; import { streamToBuffer } from 'vs/base/common/buffer'; @@ -279,39 +279,6 @@ class WorkspaceProvider implements IWorkspaceProvider { } } -class ApplicationLinkProvider { - - private links: IApplicationLink[] | undefined = undefined; - - constructor(workspace: IWorkspace) { - this.computeLink(workspace); - } - - private computeLink(workspace: IWorkspace): void { - if (!workspace) { - return; // not for empty workspaces - } - - const workspaceUri = isWorkspaceToOpen(workspace) ? workspace.workspaceUri : isFolderToOpen(workspace) ? workspace.folderUri : undefined; - if (workspaceUri) { - this.links = [{ - uri: URI.from({ - scheme: product.quality === 'stable' ? 'vscode' : 'vscode-insiders', - authority: Schemas.vscodeRemote, - path: posix.join(posix.sep, workspaceUri.authority, workspaceUri.path), - query: workspaceUri.query, - fragment: workspaceUri.fragment, - }), - label: localize('openInDesktop', "Open in Desktop") - }]; - } - } - - get provider(): IApplicationLinkProvider { - return () => this.links; - } -} - (function () { // Find config by checking for DOM @@ -375,12 +342,30 @@ class ApplicationLinkProvider { } } + // Application links ("Open in Desktop") + let applicationLinks: IApplicationLink[] | undefined = undefined; + if (workspace) { + const workspaceUri = isWorkspaceToOpen(workspace) ? workspace.workspaceUri : isFolderToOpen(workspace) ? workspace.folderUri : undefined; + if (workspaceUri) { + applicationLinks = [{ + uri: URI.from({ + scheme: product.quality === 'stable' ? 'vscode' : 'vscode-insiders', + authority: Schemas.vscodeRemote, + path: posix.join(posix.sep, workspaceUri.authority, workspaceUri.path), + query: workspaceUri.query, + fragment: workspaceUri.fragment, + }), + label: localize('openInDesktop', "Open in Desktop") + }]; + } + } + // Finally create workbench create(document.body, { ...config, workspaceProvider: new WorkspaceProvider(workspace, payload), urlCallbackProvider: new PollingURLCallbackProvider(), credentialsProvider: new LocalStorageCredentialsProvider(), - applicationLinkProvider: new ApplicationLinkProvider(workspace).provider + applicationLinks: applicationLinks }); })(); diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 600f303335e4..81611253b015 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -149,6 +149,10 @@ function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: numbe return result; } +const enum Constants { + SPAN_MODULO_LIMIT = 16384 +} + function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: IStringBuilder): [number[], number[]] { sb.appendASCIIString('
MinimapCharRenderer; - /** * container dom node left position (in CSS px) */ @@ -107,6 +91,11 @@ class MinimapOptions { */ public readonly canvasOuterHeight: number; + public readonly fontScale: number; + public readonly minimapLineHeight: number; + public readonly minimapCharWidth: number; + + public readonly charRenderer: () => MinimapCharRenderer; public readonly backgroundColor: RGBA8; constructor(configuration: IConfiguration, theme: EditorTheme, tokensColorTracker: MinimapTokensColorTracker) { @@ -114,13 +103,13 @@ class MinimapOptions { const pixelRatio = options.get(EditorOption.pixelRatio); const layoutInfo = options.get(EditorOption.layoutInfo); const fontInfo = options.get(EditorOption.fontInfo); + const minimapOpts = options.get(EditorOption.minimap); this.renderMinimap = layoutInfo.renderMinimap | 0; + this.mode = minimapOpts.mode; + this.minimapHeightIsEditorHeight = layoutInfo.minimapHeightIsEditorHeight; this.scrollBeyondLastLine = options.get(EditorOption.scrollBeyondLastLine); - const minimapOpts = options.get(EditorOption.minimap); this.showSlider = minimapOpts.showSlider; - this.fontScale = Math.round(minimapOpts.scale * pixelRatio); - this.charRenderer = once(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily)); this.pixelRatio = pixelRatio; this.typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; this.lineHeight = options.get(EditorOption.lineHeight); @@ -128,12 +117,16 @@ class MinimapOptions { this.minimapWidth = layoutInfo.minimapWidth; this.minimapHeight = layoutInfo.height; - this.canvasInnerWidth = Math.floor(pixelRatio * this.minimapWidth); - this.canvasInnerHeight = Math.floor(pixelRatio * this.minimapHeight); + this.canvasInnerWidth = layoutInfo.minimapCanvasInnerWidth; + this.canvasInnerHeight = layoutInfo.minimapCanvasInnerHeight; + this.canvasOuterWidth = layoutInfo.minimapCanvasOuterWidth; + this.canvasOuterHeight = layoutInfo.minimapCanvasOuterHeight; - this.canvasOuterWidth = this.canvasInnerWidth / pixelRatio; - this.canvasOuterHeight = this.canvasInnerHeight / pixelRatio; + this.fontScale = layoutInfo.minimapScale; + this.minimapLineHeight = layoutInfo.minimapLineHeight; + this.minimapCharWidth = Constants.BASE_CHAR_WIDTH * this.fontScale; + this.charRenderer = once(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily)); this.backgroundColor = MinimapOptions._getMinimapBackground(theme, tokensColorTracker); } @@ -147,12 +140,13 @@ class MinimapOptions { public equals(other: MinimapOptions): boolean { return (this.renderMinimap === other.renderMinimap + && this.mode === other.mode + && this.minimapHeightIsEditorHeight === other.minimapHeightIsEditorHeight && this.scrollBeyondLastLine === other.scrollBeyondLastLine && this.showSlider === other.showSlider && this.pixelRatio === other.pixelRatio && this.typicalHalfwidthCharacterWidth === other.typicalHalfwidthCharacterWidth && this.lineHeight === other.lineHeight - && this.fontScale === other.fontScale && this.minimapLeft === other.minimapLeft && this.minimapWidth === other.minimapWidth && this.minimapHeight === other.minimapHeight @@ -160,6 +154,9 @@ class MinimapOptions { && this.canvasInnerHeight === other.canvasInnerHeight && this.canvasOuterWidth === other.canvasOuterWidth && this.canvasOuterHeight === other.canvasOuterHeight + && this.fontScale === other.fontScale + && this.minimapLineHeight === other.minimapLineHeight + && this.minimapCharWidth === other.minimapCharWidth && this.backgroundColor.equals(other.backgroundColor) ); } @@ -177,6 +174,7 @@ class MinimapLayout { */ public readonly scrollHeight: number; + public readonly sliderNeeded: boolean; private readonly _computedSliderRatio: number; /** @@ -200,6 +198,7 @@ class MinimapLayout { constructor( scrollTop: number, scrollHeight: number, + sliderNeeded: boolean, computedSliderRatio: number, sliderTop: number, sliderHeight: number, @@ -208,6 +207,7 @@ class MinimapLayout { ) { this.scrollTop = scrollTop; this.scrollHeight = scrollHeight; + this.sliderNeeded = sliderNeeded; this._computedSliderRatio = computedSliderRatio; this.sliderTop = sliderTop; this.sliderHeight = sliderHeight; @@ -234,15 +234,31 @@ class MinimapLayout { viewportHeight: number, viewportContainsWhitespaceGaps: boolean, lineCount: number, + realLineCount: number, scrollTop: number, scrollHeight: number, previousLayout: MinimapLayout | null ): MinimapLayout { const pixelRatio = options.pixelRatio; - const minimapLineHeight = getMinimapLineHeight(options.renderMinimap, options.fontScale); + const minimapLineHeight = options.minimapLineHeight; const minimapLinesFitting = Math.floor(options.canvasInnerHeight / minimapLineHeight); const lineHeight = options.lineHeight; + if (options.minimapHeightIsEditorHeight) { + const logicalScrollHeight = ( + realLineCount * options.lineHeight + + (options.scrollBeyondLastLine ? viewportHeight - options.lineHeight : 0) + ); + const sliderHeight = Math.max(1, Math.floor(viewportHeight * viewportHeight / logicalScrollHeight)); + const maxMinimapSliderTop = Math.max(0, options.minimapHeight - sliderHeight); + // The slider can move from 0 to `maxMinimapSliderTop` + // in the same way `scrollTop` can move from 0 to `scrollHeight` - `viewportHeight`. + const computedSliderRatio = (maxMinimapSliderTop) / (scrollHeight - viewportHeight); + const sliderTop = (scrollTop * computedSliderRatio); + const sliderNeeded = (maxMinimapSliderTop > 0); + return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, 1, lineCount); + } + // The visible line count in a viewport can change due to a number of reasons: // a) with the same viewport width, different scroll positions can result in partial lines being visible: // e.g. for a line height of 20, and a viewport height of 600 @@ -283,14 +299,14 @@ class MinimapLayout { let extraLinesAtTheBottom = 0; if (options.scrollBeyondLastLine) { const expectedViewportLineCount = viewportHeight / lineHeight; - extraLinesAtTheBottom = expectedViewportLineCount; + extraLinesAtTheBottom = expectedViewportLineCount - 1; } if (minimapLinesFitting >= lineCount + extraLinesAtTheBottom) { // All lines fit in the minimap const startLineNumber = 1; const endLineNumber = lineCount; - - return new MinimapLayout(scrollTop, scrollHeight, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber); + const sliderNeeded = (maxMinimapSliderTop > 0); + return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber); } else { let startLineNumber = Math.max(1, Math.floor(viewportStartLineNumber - sliderTop * pixelRatio / minimapLineHeight)); @@ -309,7 +325,7 @@ class MinimapLayout { const endLineNumber = Math.min(lineCount, startLineNumber + minimapLinesFitting - 1); - return new MinimapLayout(scrollTop, scrollHeight, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber); + return new MinimapLayout(scrollTop, scrollHeight, true, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber); } } } @@ -391,17 +407,17 @@ class RenderData { }; } - public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { - return this._renderedLines.onLinesChanged(e.fromLineNumber, e.toLineNumber); + public onLinesChanged(changeFromLineNumber: number, changeToLineNumber: number): boolean { + return this._renderedLines.onLinesChanged(changeFromLineNumber, changeToLineNumber); } - public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): void { - this._renderedLines.onLinesDeleted(e.fromLineNumber, e.toLineNumber); + public onLinesDeleted(deleteFromLineNumber: number, deleteToLineNumber: number): void { + this._renderedLines.onLinesDeleted(deleteFromLineNumber, deleteToLineNumber); } - public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): void { - this._renderedLines.onLinesInserted(e.fromLineNumber, e.toLineNumber); + public onLinesInserted(insertFromLineNumber: number, insertToLineNumber: number): void { + this._renderedLines.onLinesInserted(insertFromLineNumber, insertToLineNumber); } - public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean { - return this._renderedLines.onTokensChanged(e.ranges); + public onTokensChanged(ranges: { fromLineNumber: number; toLineNumber: number; }[]): boolean { + return this._renderedLines.onTokensChanged(ranges); } } @@ -458,9 +474,557 @@ class MinimapBuffers { } } -export class Minimap extends ViewPart { +export interface IMinimapModel { + readonly tokensColorTracker: MinimapTokensColorTracker; + readonly options: MinimapOptions; + + getLineCount(): number; + getRealLineCount(): number; + getLineContent(lineNumber: number): string; + getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[]; + getSelections(): Selection[]; + getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[]; + getOptions(): TextModelResolvedOptions; + revealLineNumber(lineNumber: number): void; + setScrollTop(scrollTop: number): void; +} + +interface IMinimapRenderingContext { + readonly viewportContainsWhitespaceGaps: boolean; + + readonly scrollWidth: number; + readonly scrollHeight: number; + + readonly viewportStartLineNumber: number; + readonly viewportEndLineNumber: number; + + readonly scrollTop: number; + readonly scrollLeft: number; + + readonly viewportWidth: number; + readonly viewportHeight: number; +} + +interface SamplingStateLinesDeletedEvent { + type: 'deleted'; + _oldIndex: number; + deleteFromLineNumber: number; + deleteToLineNumber: number; +} + +interface SamplingStateLinesInsertedEvent { + type: 'inserted'; + _i: number; + insertFromLineNumber: number; + insertToLineNumber: number; +} + +interface SamplingStateFlushEvent { + type: 'flush'; +} + +type SamplingStateEvent = SamplingStateLinesInsertedEvent | SamplingStateLinesDeletedEvent | SamplingStateFlushEvent; + +class MinimapSamplingState { + + public static compute(options: IComputedEditorOptions, modelLineCount: number, oldSamplingState: MinimapSamplingState | null): [MinimapSamplingState | null, SamplingStateEvent[]] { + const minimapOpts = options.get(EditorOption.minimap); + const layoutInfo = options.get(EditorOption.layoutInfo); + if (!minimapOpts.enabled || !layoutInfo.minimapIsSampling) { + return [null, []]; + } + + // ratio is intentionally not part of the layout to avoid the layout changing all the time + // so we need to recompute it again... + const pixelRatio = options.get(EditorOption.pixelRatio); + const lineHeight = options.get(EditorOption.lineHeight); + const scrollBeyondLastLine = options.get(EditorOption.scrollBeyondLastLine); + const { minimapLineCount } = EditorLayoutInfoComputer.computeContainedMinimapLineCount({ + modelLineCount: modelLineCount, + scrollBeyondLastLine: scrollBeyondLastLine, + height: layoutInfo.height, + lineHeight: lineHeight, + pixelRatio: pixelRatio + }); + const ratio = modelLineCount / minimapLineCount; + const halfRatio = ratio / 2; + + if (!oldSamplingState || oldSamplingState.minimapLines.length === 0) { + let result: number[] = []; + result[0] = 1; + if (minimapLineCount > 1) { + for (let i = 0, lastIndex = minimapLineCount - 1; i < lastIndex; i++) { + result[i] = Math.round(i * ratio + halfRatio); + } + result[minimapLineCount - 1] = modelLineCount; + } + return [new MinimapSamplingState(ratio, result), []]; + } + + const oldMinimapLines = oldSamplingState.minimapLines; + const oldLength = oldMinimapLines.length; + let result: number[] = []; + let oldIndex = 0; + let oldDeltaLineCount = 0; + let minModelLineNumber = 1; + const MAX_EVENT_COUNT = 10; // generate at most 10 events, if there are more than 10 changes, just flush all previous data + let events: SamplingStateEvent[] = []; + let lastEvent: SamplingStateEvent | null = null; + for (let i = 0; i < minimapLineCount; i++) { + const fromModelLineNumber = Math.max(minModelLineNumber, Math.round(i * ratio)); + const toModelLineNumber = Math.max(fromModelLineNumber, Math.round((i + 1) * ratio)); + + while (oldIndex < oldLength && oldMinimapLines[oldIndex] < fromModelLineNumber) { + if (events.length < MAX_EVENT_COUNT) { + const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount; + if (lastEvent && lastEvent.type === 'deleted' && lastEvent._oldIndex === oldIndex - 1) { + lastEvent.deleteToLineNumber++; + } else { + lastEvent = { type: 'deleted', _oldIndex: oldIndex, deleteFromLineNumber: oldMinimapLineNumber, deleteToLineNumber: oldMinimapLineNumber }; + events.push(lastEvent); + } + oldDeltaLineCount--; + } + oldIndex++; + } + + let selectedModelLineNumber: number; + if (oldIndex < oldLength && oldMinimapLines[oldIndex] <= toModelLineNumber) { + // reuse the old sampled line + selectedModelLineNumber = oldMinimapLines[oldIndex]; + oldIndex++; + } else { + if (i === 0) { + selectedModelLineNumber = 1; + } else if (i + 1 === minimapLineCount) { + selectedModelLineNumber = modelLineCount; + } else { + selectedModelLineNumber = Math.round(i * ratio + halfRatio); + } + if (events.length < MAX_EVENT_COUNT) { + const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount; + if (lastEvent && lastEvent.type === 'inserted' && lastEvent._i === i - 1) { + lastEvent.insertToLineNumber++; + } else { + lastEvent = { type: 'inserted', _i: i, insertFromLineNumber: oldMinimapLineNumber, insertToLineNumber: oldMinimapLineNumber }; + events.push(lastEvent); + } + oldDeltaLineCount++; + } + } + + result[i] = selectedModelLineNumber; + minModelLineNumber = selectedModelLineNumber; + } + + if (events.length < MAX_EVENT_COUNT) { + while (oldIndex < oldLength) { + const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount; + if (lastEvent && lastEvent.type === 'deleted' && lastEvent._oldIndex === oldIndex - 1) { + lastEvent.deleteToLineNumber++; + } else { + lastEvent = { type: 'deleted', _oldIndex: oldIndex, deleteFromLineNumber: oldMinimapLineNumber, deleteToLineNumber: oldMinimapLineNumber }; + events.push(lastEvent); + } + oldDeltaLineCount--; + oldIndex++; + } + } else { + // too many events, just give up + events = [{ type: 'flush' }]; + } + + return [new MinimapSamplingState(ratio, result), events]; + } + + constructor( + public readonly samplingRatio: number, + public readonly minimapLines: number[] + ) { + } + + public modelLineToMinimapLine(lineNumber: number): number { + return Math.min(this.minimapLines.length, Math.max(1, Math.round(lineNumber / this.samplingRatio))); + } + + /** + * Will return null if the model line ranges are not intersecting with a sampled model line. + */ + public modelLineRangeToMinimapLineRange(fromLineNumber: number, toLineNumber: number): [number, number] | null { + let fromLineIndex = this.modelLineToMinimapLine(fromLineNumber) - 1; + while (fromLineIndex > 0 && this.minimapLines[fromLineIndex - 1] >= fromLineNumber) { + fromLineIndex--; + } + let toLineIndex = this.modelLineToMinimapLine(toLineNumber) - 1; + while (toLineIndex + 1 < this.minimapLines.length && this.minimapLines[toLineIndex + 1] <= toLineNumber) { + toLineIndex++; + } + if (fromLineIndex === toLineIndex) { + const sampledLineNumber = this.minimapLines[fromLineIndex]; + if (sampledLineNumber < fromLineNumber || sampledLineNumber > toLineNumber) { + // This line is not part of the sampled lines ==> nothing to do + return null; + } + } + return [fromLineIndex + 1, toLineIndex + 1]; + } + + /** + * Will always return a range, even if it is not intersecting with a sampled model line. + */ + public decorationLineRangeToMinimapLineRange(startLineNumber: number, endLineNumber: number): [number, number] { + let minimapLineStart = this.modelLineToMinimapLine(startLineNumber); + let minimapLineEnd = this.modelLineToMinimapLine(endLineNumber); + if (startLineNumber !== endLineNumber && minimapLineEnd === minimapLineStart) { + if (minimapLineEnd === this.minimapLines.length) { + if (minimapLineStart > 1) { + minimapLineStart--; + } + } else { + minimapLineEnd++; + } + } + return [minimapLineStart, minimapLineEnd]; + } + + public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): [number, number] { + // have the mapping be sticky + const deletedLineCount = e.toLineNumber - e.fromLineNumber + 1; + let changeStartIndex = this.minimapLines.length; + let changeEndIndex = 0; + for (let i = this.minimapLines.length - 1; i >= 0; i--) { + if (this.minimapLines[i] < e.fromLineNumber) { + break; + } + if (this.minimapLines[i] <= e.toLineNumber) { + // this line got deleted => move to previous available + this.minimapLines[i] = Math.max(1, e.fromLineNumber - 1); + changeStartIndex = Math.min(changeStartIndex, i); + changeEndIndex = Math.max(changeEndIndex, i); + } else { + this.minimapLines[i] -= deletedLineCount; + } + } + return [changeStartIndex, changeEndIndex]; + } + + public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): void { + // have the mapping be sticky + const insertedLineCount = e.toLineNumber - e.fromLineNumber + 1; + for (let i = this.minimapLines.length - 1; i >= 0; i--) { + if (this.minimapLines[i] < e.fromLineNumber) { + break; + } + this.minimapLines[i] += insertedLineCount; + } + } +} + +export class Minimap extends ViewPart implements IMinimapModel { + + public readonly tokensColorTracker: MinimapTokensColorTracker; + + private _selections: Selection[]; + private _minimapSelections: Selection[] | null; + + public options: MinimapOptions; + + private _samplingState: MinimapSamplingState | null; + private _shouldCheckSampling: boolean; + + private _actual: InnerMinimap; + + constructor(context: ViewContext) { + super(context); + + this.tokensColorTracker = MinimapTokensColorTracker.getInstance(); + + this._selections = []; + this._minimapSelections = null; + + this.options = new MinimapOptions(this._context.configuration, this._context.theme, this.tokensColorTracker); + const [samplingState,] = MinimapSamplingState.compute(this._context.configuration.options, this._context.model.getLineCount(), null); + this._samplingState = samplingState; + this._shouldCheckSampling = false; + + this._actual = new InnerMinimap(context.theme, this); + } + + public dispose(): void { + this._actual.dispose(); + super.dispose(); + } + + public getDomNode(): FastDomNode { + return this._actual.getDomNode(); + } + + private _onOptionsMaybeChanged(): boolean { + const opts = new MinimapOptions(this._context.configuration, this._context.theme, this.tokensColorTracker); + if (this.options.equals(opts)) { + return false; + } + this.options = opts; + this._recreateLineSampling(); + this._actual.onDidChangeOptions(); + return true; + } + + // ---- begin view event handlers + + public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { + return this._onOptionsMaybeChanged(); + } + public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { + this._selections = e.selections; + this._minimapSelections = null; + return this._actual.onSelectionChanged(); + } + public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { + if (e.affectsMinimap) { + return this._actual.onDecorationsChanged(); + } + return false; + } + public onFlushed(e: viewEvents.ViewFlushedEvent): boolean { + return this._actual.onFlushed(); + } + public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { + if (this._samplingState) { + const minimapLineRange = this._samplingState.modelLineRangeToMinimapLineRange(e.fromLineNumber, e.toLineNumber); + if (minimapLineRange) { + return this._actual.onLinesChanged(minimapLineRange[0], minimapLineRange[1]); + } else { + return false; + } + } else { + return this._actual.onLinesChanged(e.fromLineNumber, e.toLineNumber); + } + } + public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean { + if (this._samplingState) { + const [changeStartIndex, changeEndIndex] = this._samplingState.onLinesDeleted(e); + if (changeStartIndex <= changeEndIndex) { + this._actual.onLinesChanged(changeStartIndex + 1, changeEndIndex + 1); + } + this._shouldCheckSampling = true; + return true; + } else { + return this._actual.onLinesDeleted(e.fromLineNumber, e.toLineNumber); + } + } + public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean { + if (this._samplingState) { + this._samplingState.onLinesInserted(e); + this._shouldCheckSampling = true; + return true; + } else { + return this._actual.onLinesInserted(e.fromLineNumber, e.toLineNumber); + } + } + public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { + return this._actual.onScrollChanged(); + } + public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { + this._context.model.invalidateMinimapColorCache(); + this._actual.onThemeChanged(); + this._onOptionsMaybeChanged(); + return true; + } + public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean { + if (this._samplingState) { + let ranges: { fromLineNumber: number; toLineNumber: number; }[] = []; + for (const range of e.ranges) { + const minimapLineRange = this._samplingState.modelLineRangeToMinimapLineRange(range.fromLineNumber, range.toLineNumber); + if (minimapLineRange) { + ranges.push({ fromLineNumber: minimapLineRange[0], toLineNumber: minimapLineRange[1] }); + } + } + if (ranges.length) { + return this._actual.onTokensChanged(ranges); + } else { + return false; + } + } else { + return this._actual.onTokensChanged(e.ranges); + } + } + public onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean { + return this._actual.onTokensColorsChanged(); + } + public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { + return this._actual.onZonesChanged(); + } + + // --- end event handlers + + public prepareRender(ctx: RenderingContext): void { + if (this._shouldCheckSampling) { + this._shouldCheckSampling = false; + this._recreateLineSampling(); + } + } + + public render(ctx: RestrictedRenderingContext): void { + let viewportStartLineNumber = ctx.visibleRange.startLineNumber; + let viewportEndLineNumber = ctx.visibleRange.endLineNumber; + + if (this._samplingState) { + viewportStartLineNumber = this._samplingState.modelLineToMinimapLine(viewportStartLineNumber); + viewportEndLineNumber = this._samplingState.modelLineToMinimapLine(viewportEndLineNumber); + } + + const minimapCtx: IMinimapRenderingContext = { + viewportContainsWhitespaceGaps: (ctx.viewportData.whitespaceViewportData.length > 0), + + scrollWidth: ctx.scrollWidth, + scrollHeight: ctx.scrollHeight, + + viewportStartLineNumber: viewportStartLineNumber, + viewportEndLineNumber: viewportEndLineNumber, + + scrollTop: ctx.scrollTop, + scrollLeft: ctx.scrollLeft, + + viewportWidth: ctx.viewportWidth, + viewportHeight: ctx.viewportHeight, + }; + this._actual.render(minimapCtx); + } + + //#region IMinimapModel + + private _recreateLineSampling(): void { + this._minimapSelections = null; + + const wasSampling = Boolean(this._samplingState); + const [samplingState, events] = MinimapSamplingState.compute(this._context.configuration.options, this._context.model.getLineCount(), this._samplingState); + this._samplingState = samplingState; + + if (wasSampling && this._samplingState) { + // was sampling, is sampling + for (const event of events) { + switch (event.type) { + case 'deleted': + this._actual.onLinesDeleted(event.deleteFromLineNumber, event.deleteToLineNumber); + break; + case 'inserted': + this._actual.onLinesInserted(event.insertFromLineNumber, event.insertToLineNumber); + break; + case 'flush': + this._actual.onFlushed(); + break; + } + } + } + } + + public getLineCount(): number { + if (this._samplingState) { + return this._samplingState.minimapLines.length; + } + return this._context.model.getLineCount(); + } + + public getRealLineCount(): number { + return this._context.model.getLineCount(); + } + + public getLineContent(lineNumber: number): string { + if (this._samplingState) { + return this._context.model.getLineContent(this._samplingState.minimapLines[lineNumber - 1]); + } + return this._context.model.getLineContent(lineNumber); + } + + public getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[] { + if (this._samplingState) { + let result: (ViewLineData | null)[] = []; + for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) { + if (needed[lineIndex]) { + result[lineIndex] = this._context.model.getViewLineData(this._samplingState.minimapLines[startLineNumber + lineIndex - 1]); + } else { + result[lineIndex] = null; + } + } + return result; + } + return this._context.model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed).data; + } + + public getSelections(): Selection[] { + if (this._minimapSelections === null) { + if (this._samplingState) { + this._minimapSelections = []; + for (const selection of this._selections) { + const [minimapLineStart, minimapLineEnd] = this._samplingState.decorationLineRangeToMinimapLineRange(selection.startLineNumber, selection.endLineNumber); + this._minimapSelections.push(new Selection(minimapLineStart, selection.startColumn, minimapLineEnd, selection.endColumn)); + } + } else { + this._minimapSelections = this._selections; + } + } + return this._minimapSelections; + } + + public getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[] { + let visibleRange: Range; + if (this._samplingState) { + const modelStartLineNumber = this._samplingState.minimapLines[startLineNumber - 1]; + const modelEndLineNumber = this._samplingState.minimapLines[endLineNumber - 1]; + visibleRange = new Range(modelStartLineNumber, 1, modelEndLineNumber, this._context.model.getLineMaxColumn(modelEndLineNumber)); + } else { + visibleRange = new Range(startLineNumber, 1, endLineNumber, this._context.model.getLineMaxColumn(endLineNumber)); + } + const decorations = this._context.model.getDecorationsInViewport(visibleRange); + + if (this._samplingState) { + let result: ViewModelDecoration[] = []; + for (const decoration of decorations) { + if (!decoration.options.minimap) { + continue; + } + const range = decoration.range; + const minimapStartLineNumber = this._samplingState.modelLineToMinimapLine(range.startLineNumber); + const minimapEndLineNumber = this._samplingState.modelLineToMinimapLine(range.endLineNumber); + result.push(new ViewModelDecoration(new Range(minimapStartLineNumber, range.startColumn, minimapEndLineNumber, range.endColumn), decoration.options)); + } + return result; + } + return decorations; + } + + public getOptions(): TextModelResolvedOptions { + return this._context.model.getOptions(); + } + + public revealLineNumber(lineNumber: number): void { + if (this._samplingState) { + lineNumber = this._samplingState.minimapLines[lineNumber - 1]; + } + this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent( + 'mouse', + new Range(lineNumber, 1, lineNumber, 1), + viewEvents.VerticalRevealType.Center, + false, + ScrollType.Smooth + )); + } + + public setScrollTop(scrollTop: number): void { + this._context.viewLayout.setScrollPositionNow({ + scrollTop: scrollTop + }); + } + + //#endregion +} + +class InnerMinimap extends Disposable { + + private readonly _theme: EditorTheme; + private readonly _model: IMinimapModel; - private readonly _tokensColorTracker: MinimapTokensColorTracker; private readonly _domNode: FastDomNode; private readonly _shadow: FastDomNode; private readonly _canvas: FastDomNode; @@ -475,22 +1039,24 @@ export class Minimap extends ViewPart { private readonly _sliderTouchMoveListener: IDisposable; private readonly _sliderTouchEndListener: IDisposable; - private _options: MinimapOptions; private _lastRenderData: RenderData | null; - private _selections: Selection[] = []; private _selectionColor: Color | undefined; private _renderDecorations: boolean = false; private _gestureInProgress: boolean = false; private _buffers: MinimapBuffers | null; - constructor(context: ViewContext) { - super(context); + constructor( + theme: EditorTheme, + model: IMinimapModel + ) { + super(); + + this._theme = theme; + this._model = model; - this._tokensColorTracker = MinimapTokensColorTracker.getInstance(); - this._options = new MinimapOptions(this._context.configuration, this._context.theme, this._tokensColorTracker); this._lastRenderData = null; this._buffers = null; - this._selectionColor = this._context.theme.getColor(minimapSelection); + this._selectionColor = this._theme.getColor(minimapSelection); this._domNode = createFastDomNode(document.createElement('div')); PartFingerprints.write(this._domNode, PartFingerprint.Minimap); @@ -531,27 +1097,30 @@ export class Minimap extends ViewPart { this._mouseDownListener = dom.addStandardDisposableListener(this._domNode.domNode, 'mousedown', (e) => { e.preventDefault(); - const renderMinimap = this._options.renderMinimap; + const renderMinimap = this._model.options.renderMinimap; if (renderMinimap === RenderMinimap.None) { return; } if (!this._lastRenderData) { return; } - const minimapLineHeight = getMinimapLineHeight(renderMinimap, this._options.fontScale); - const internalOffsetY = this._options.pixelRatio * e.browserEvent.offsetY; + if (this._model.options.minimapHeightIsEditorHeight) { + if (e.leftButton && this._lastRenderData) { + // pretend the click occured in the center of the slider + const position = dom.getDomNodePagePosition(this._slider.domNode); + const initialPosY = position.top + position.height / 2; + this._startSliderDragging(e.buttons, e.posx, initialPosY, e.posy, this._lastRenderData.renderedLayout); + } + return; + } + const minimapLineHeight = this._model.options.minimapLineHeight; + const internalOffsetY = (this._model.options.canvasInnerHeight / this._model.options.canvasOuterHeight) * e.browserEvent.offsetY; const lineIndex = Math.floor(internalOffsetY / minimapLineHeight); let lineNumber = lineIndex + this._lastRenderData.renderedLayout.startLineNumber; - lineNumber = Math.min(lineNumber, this._context.model.getLineCount()); + lineNumber = Math.min(lineNumber, this._model.getLineCount()); - this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent( - 'mouse', - new Range(lineNumber, 1, lineNumber, 1), - viewEvents.VerticalRevealType.Center, - false, - ScrollType.Smooth - )); + this._model.revealLineNumber(lineNumber); }); this._sliderMouseMoveMonitor = new GlobalMouseMoveMonitor(); @@ -560,36 +1129,7 @@ export class Minimap extends ViewPart { e.preventDefault(); e.stopPropagation(); if (e.leftButton && this._lastRenderData) { - - const initialMousePosition = e.posy; - const initialMouseOrthogonalPosition = e.posx; - const initialSliderState = this._lastRenderData.renderedLayout; - this._slider.toggleClassName('active', true); - - this._sliderMouseMoveMonitor.startMonitoring( - e.target, - e.buttons, - standardMouseMoveMerger, - (mouseMoveData: IStandardMouseMoveEventData) => { - const mouseOrthogonalDelta = Math.abs(mouseMoveData.posx - initialMouseOrthogonalPosition); - - if (platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) { - // The mouse has wondered away from the scrollbar => reset dragging - this._context.viewLayout.setScrollPositionNow({ - scrollTop: initialSliderState.scrollTop - }); - return; - } - - const mouseDelta = mouseMoveData.posy - initialMousePosition; - this._context.viewLayout.setScrollPositionNow({ - scrollTop: initialSliderState.getDesiredScrollTopFromDelta(mouseDelta) - }); - }, - () => { - this._slider.toggleClassName('active', false); - } - ); + this._startSliderDragging(e.buttons, e.posx, e.posy, e.posy, this._lastRenderData.renderedLayout); } }); @@ -620,12 +1160,41 @@ export class Minimap extends ViewPart { }); } + private _startSliderDragging(initialButtons: number, initialPosX: number, initialPosY: number, posy: number, initialSliderState: MinimapLayout): void { + this._slider.toggleClassName('active', true); + + const handleMouseMove = (posy: number, posx: number) => { + const mouseOrthogonalDelta = Math.abs(posx - initialPosX); + + if (platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) { + // The mouse has wondered away from the scrollbar => reset dragging + this._model.setScrollTop(initialSliderState.scrollTop); + return; + } + + const mouseDelta = posy - initialPosY; + this._model.setScrollTop(initialSliderState.getDesiredScrollTopFromDelta(mouseDelta)); + }; + + if (posy !== initialPosY) { + handleMouseMove(posy, initialPosX); + } + + this._sliderMouseMoveMonitor.startMonitoring( + this._slider.domNode, + initialButtons, + standardMouseMoveMerger, + (mouseMoveData: IStandardMouseMoveEventData) => handleMouseMove(mouseMoveData.posy, mouseMoveData.posx), + () => { + this._slider.toggleClassName('active', false); + } + ); + } + private scrollDueToTouchEvent(touch: GestureEvent) { const startY = this._domNode.domNode.getBoundingClientRect().top; const scrollTop = this._lastRenderData!.renderedLayout.getDesiredScrollTopFromTouchLocation(touch.pageY - startY); - this._context.viewLayout.setScrollPositionNow({ - scrollTop: scrollTop - }); + this._model.setScrollTop(scrollTop); } public dispose(): void { @@ -640,7 +1209,7 @@ export class Minimap extends ViewPart { } private _getMinimapDomNodeClassName(): string { - if (this._options.showSlider === 'always') { + if (this._model.options.showSlider === 'always') { return 'minimap slider-always'; } return 'minimap slider-mouseover'; @@ -651,124 +1220,105 @@ export class Minimap extends ViewPart { } private _applyLayout(): void { - this._domNode.setLeft(this._options.minimapLeft); - this._domNode.setWidth(this._options.minimapWidth); - this._domNode.setHeight(this._options.minimapHeight); - this._shadow.setHeight(this._options.minimapHeight); + this._domNode.setLeft(this._model.options.minimapLeft); + this._domNode.setWidth(this._model.options.minimapWidth); + this._domNode.setHeight(this._model.options.minimapHeight); + this._shadow.setHeight(this._model.options.minimapHeight); - this._canvas.setWidth(this._options.canvasOuterWidth); - this._canvas.setHeight(this._options.canvasOuterHeight); - this._canvas.domNode.width = this._options.canvasInnerWidth; - this._canvas.domNode.height = this._options.canvasInnerHeight; + this._canvas.setWidth(this._model.options.canvasOuterWidth); + this._canvas.setHeight(this._model.options.canvasOuterHeight); + this._canvas.domNode.width = this._model.options.canvasInnerWidth; + this._canvas.domNode.height = this._model.options.canvasInnerHeight; - this._decorationsCanvas.setWidth(this._options.canvasOuterWidth); - this._decorationsCanvas.setHeight(this._options.canvasOuterHeight); - this._decorationsCanvas.domNode.width = this._options.canvasInnerWidth; - this._decorationsCanvas.domNode.height = this._options.canvasInnerHeight; + this._decorationsCanvas.setWidth(this._model.options.canvasOuterWidth); + this._decorationsCanvas.setHeight(this._model.options.canvasOuterHeight); + this._decorationsCanvas.domNode.width = this._model.options.canvasInnerWidth; + this._decorationsCanvas.domNode.height = this._model.options.canvasInnerHeight; - this._slider.setWidth(this._options.minimapWidth); + this._slider.setWidth(this._model.options.minimapWidth); } private _getBuffer(): ImageData | null { if (!this._buffers) { - if (this._options.canvasInnerWidth > 0 && this._options.canvasInnerHeight > 0) { + if (this._model.options.canvasInnerWidth > 0 && this._model.options.canvasInnerHeight > 0) { this._buffers = new MinimapBuffers( this._canvas.domNode.getContext('2d')!, - this._options.canvasInnerWidth, - this._options.canvasInnerHeight, - this._options.backgroundColor + this._model.options.canvasInnerWidth, + this._model.options.canvasInnerHeight, + this._model.options.backgroundColor ); } } return this._buffers ? this._buffers.getBuffer() : null; } - private _onOptionsMaybeChanged(): boolean { - const opts = new MinimapOptions(this._context.configuration, this._context.theme, this._tokensColorTracker); - if (this._options.equals(opts)) { - return false; - } - this._options = opts; + // ---- begin view event handlers + + public onDidChangeOptions(): void { this._lastRenderData = null; this._buffers = null; this._applyLayout(); this._domNode.setClassName(this._getMinimapDomNodeClassName()); - return true; } - - // ---- begin view event handlers - - public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { - return this._onOptionsMaybeChanged(); - } - public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { - this._selections = e.selections; + public onSelectionChanged(): boolean { this._renderDecorations = true; return true; } - public onFlushed(e: viewEvents.ViewFlushedEvent): boolean { + public onDecorationsChanged(): boolean { + this._renderDecorations = true; + return true; + } + public onFlushed(): boolean { this._lastRenderData = null; return true; } - public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { + public onLinesChanged(changeFromLineNumber: number, changeToLineNumber: number): boolean { if (this._lastRenderData) { - return this._lastRenderData.onLinesChanged(e); + return this._lastRenderData.onLinesChanged(changeFromLineNumber, changeToLineNumber); } return false; } - public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean { + public onLinesDeleted(deleteFromLineNumber: number, deleteToLineNumber: number): boolean { if (this._lastRenderData) { - this._lastRenderData.onLinesDeleted(e); + this._lastRenderData.onLinesDeleted(deleteFromLineNumber, deleteToLineNumber); } return true; } - public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean { + public onLinesInserted(insertFromLineNumber: number, insertToLineNumber: number): boolean { if (this._lastRenderData) { - this._lastRenderData.onLinesInserted(e); + this._lastRenderData.onLinesInserted(insertFromLineNumber, insertToLineNumber); } return true; } - public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { + public onScrollChanged(): boolean { this._renderDecorations = true; return true; } - public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean { + public onThemeChanged(): boolean { + this._selectionColor = this._theme.getColor(minimapSelection); + this._renderDecorations = true; + return true; + } + public onTokensChanged(ranges: { fromLineNumber: number; toLineNumber: number; }[]): boolean { if (this._lastRenderData) { - return this._lastRenderData.onTokensChanged(e); + return this._lastRenderData.onTokensChanged(ranges); } return false; } - public onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean { + public onTokensColorsChanged(): boolean { this._lastRenderData = null; this._buffers = null; return true; } - public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { + public onZonesChanged(): boolean { this._lastRenderData = null; return true; } - public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { - this._renderDecorations = true; - return true; - } - - public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { - this._context.model.invalidateMinimapColorCache(); - this._selectionColor = this._context.theme.getColor(minimapSelection); - this._renderDecorations = true; - this._onOptionsMaybeChanged(); - return true; - } - // --- end event handlers - public prepareRender(ctx: RenderingContext): void { - // Nothing to read - } - - public render(renderingCtx: RestrictedRenderingContext): void { - const renderMinimap = this._options.renderMinimap; + public render(renderingCtx: IMinimapRenderingContext): void { + const renderMinimap = this._model.options.renderMinimap; if (renderMinimap === RenderMinimap.None) { this._shadow.setClassName('minimap-shadow-hidden'); this._sliderHorizontal.setWidth(0); @@ -782,24 +1332,26 @@ export class Minimap extends ViewPart { } const layout = MinimapLayout.create( - this._options, - renderingCtx.visibleRange.startLineNumber, - renderingCtx.visibleRange.endLineNumber, + this._model.options, + renderingCtx.viewportStartLineNumber, + renderingCtx.viewportEndLineNumber, renderingCtx.viewportHeight, - (renderingCtx.viewportData.whitespaceViewportData.length > 0), - this._context.model.getLineCount(), + renderingCtx.viewportContainsWhitespaceGaps, + this._model.getLineCount(), + this._model.getRealLineCount(), renderingCtx.scrollTop, renderingCtx.scrollHeight, this._lastRenderData ? this._lastRenderData.renderedLayout : null ); + this._slider.setDisplay(layout.sliderNeeded ? 'block' : 'none'); this._slider.setTop(layout.sliderTop); this._slider.setHeight(layout.sliderHeight); // Compute horizontal slider coordinates - const scrollLeftChars = renderingCtx.scrollLeft / this._options.typicalHalfwidthCharacterWidth; - const horizontalSliderLeft = Math.min(this._options.minimapWidth, Math.round(scrollLeftChars * getMinimapCharWidth(this._options.renderMinimap, this._options.fontScale) / this._options.pixelRatio)); + const scrollLeftChars = renderingCtx.scrollLeft / this._model.options.typicalHalfwidthCharacterWidth; + const horizontalSliderLeft = Math.min(this._model.options.minimapWidth, Math.round(scrollLeftChars * this._model.options.minimapCharWidth / this._model.options.pixelRatio)); this._sliderHorizontal.setLeft(horizontalSliderLeft); - this._sliderHorizontal.setWidth(this._options.minimapWidth - horizontalSliderLeft); + this._sliderHorizontal.setWidth(this._model.options.minimapWidth - horizontalSliderLeft); this._sliderHorizontal.setTop(0); this._sliderHorizontal.setHeight(layout.sliderHeight); @@ -810,19 +1362,20 @@ export class Minimap extends ViewPart { private renderDecorations(layout: MinimapLayout) { if (this._renderDecorations) { this._renderDecorations = false; - const decorations = this._context.model.getDecorationsInViewport(new Range(layout.startLineNumber, 1, layout.endLineNumber, this._context.model.getLineMaxColumn(layout.endLineNumber))); + const selections = this._model.getSelections(); + const decorations = this._model.getMinimapDecorationsInViewport(layout.startLineNumber, layout.endLineNumber); - const { renderMinimap, canvasInnerWidth, canvasInnerHeight } = this._options; - const lineHeight = getMinimapLineHeight(renderMinimap, this._options.fontScale); - const characterWidth = getMinimapCharWidth(renderMinimap, this._options.fontScale); - const tabSize = this._context.model.getOptions().tabSize; + const { canvasInnerWidth, canvasInnerHeight } = this._model.options; + const lineHeight = this._model.options.minimapLineHeight; + const characterWidth = this._model.options.minimapCharWidth; + const tabSize = this._model.getOptions().tabSize; const canvasContext = this._decorationsCanvas.domNode.getContext('2d')!; canvasContext.clearRect(0, 0, canvasInnerWidth, canvasInnerHeight); const lineOffsetMap = new Map(); - for (let i = 0; i < this._selections.length; i++) { - const selection = this._selections[i]; + for (let i = 0; i < selections.length; i++) { + const selection = selections[i]; for (let line = selection.startLineNumber; line <= selection.endLineNumber; line++) { this.renderDecorationOnLine(canvasContext, lineOffsetMap, selection, this._selectionColor, layout, line, lineHeight, lineHeight, tabSize, characterWidth); @@ -837,7 +1390,7 @@ export class Minimap extends ViewPart { continue; } - const decorationColor = (decoration.options.minimap).getColor(this._context.theme); + const decorationColor = (decoration.options.minimap).getColor(this._theme); for (let line = decoration.range.startLineNumber; line <= decoration.range.endLineNumber; line++) { switch (decoration.options.minimap.position) { @@ -869,7 +1422,7 @@ export class Minimap extends ViewPart { const y = (lineNumber - layout.startLineNumber) * lineHeight; // Skip rendering the line if it's vertically outside our viewport - if (y + height < 0 || y > this._options.canvasInnerHeight) { + if (y + height < 0 || y > this._model.options.canvasInnerHeight) { return; } @@ -877,7 +1430,7 @@ export class Minimap extends ViewPart { let lineIndexToXOffset = lineOffsetMap.get(lineNumber); const isFirstDecorationForLine = !lineIndexToXOffset; if (!lineIndexToXOffset) { - const lineData = this._context.model.getLineContent(lineNumber); + const lineData = this._model.getLineContent(lineNumber); lineIndexToXOffset = [MINIMAP_GUTTER_WIDTH]; for (let i = 1; i < lineData.length + 1; i++) { const charCode = lineData.charCodeAt(i - 1); @@ -922,11 +1475,9 @@ export class Minimap extends ViewPart { } private renderLines(layout: MinimapLayout): RenderData | null { - const renderMinimap = this._options.renderMinimap; - const charRenderer = this._options.charRenderer(); const startLineNumber = layout.startLineNumber; const endLineNumber = layout.endLineNumber; - const minimapLineHeight = getMinimapLineHeight(renderMinimap, this._options.fontScale); + const minimapLineHeight = this._model.options.minimapLineHeight; // Check if nothing changed w.r.t. lines from last frame if (this._lastRenderData && this._lastRenderData.linesEquals(layout)) { @@ -944,7 +1495,7 @@ export class Minimap extends ViewPart { } // Render untouched lines by using last rendered data. - let [_dirtyY1, _dirtyY2, needed] = Minimap._renderUntouchedLines( + let [_dirtyY1, _dirtyY2, needed] = InnerMinimap._renderUntouchedLines( imageData, startLineNumber, endLineNumber, @@ -953,27 +1504,39 @@ export class Minimap extends ViewPart { ); // Fetch rendering info from view model for rest of lines that need rendering. - const lineInfo = this._context.model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed); - const tabSize = lineInfo.tabSize; - const background = this._options.backgroundColor; - const useLighterFont = this._tokensColorTracker.backgroundIsLight(); + const lineInfo = this._model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed); + const tabSize = this._model.getOptions().tabSize; + const background = this._model.options.backgroundColor; + const tokensColorTracker = this._model.tokensColorTracker; + const useLighterFont = tokensColorTracker.backgroundIsLight(); + const renderMinimap = this._model.options.renderMinimap; + const charRenderer = this._model.options.charRenderer(); + const fontScale = this._model.options.fontScale; + const minimapCharWidth = this._model.options.minimapCharWidth; + + const baseCharHeight = (renderMinimap === RenderMinimap.Text ? Constants.BASE_CHAR_HEIGHT : Constants.BASE_CHAR_HEIGHT + 1); + const renderMinimapLineHeight = baseCharHeight * fontScale; + const innerLinePadding = (minimapLineHeight > renderMinimapLineHeight ? Math.floor((minimapLineHeight - renderMinimapLineHeight) / 2) : 0); // Render the rest of lines let dy = 0; const renderedLines: MinimapLine[] = []; for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) { if (needed[lineIndex]) { - Minimap._renderLine( + InnerMinimap._renderLine( imageData, background, useLighterFont, renderMinimap, - this._tokensColorTracker, + minimapCharWidth, + tokensColorTracker, charRenderer, dy, + innerLinePadding, tabSize, - lineInfo.data[lineIndex]!, - this._options.fontScale + lineInfo[lineIndex]!, + fontScale, + minimapLineHeight ); } renderedLines[lineIndex] = new MinimapLine(dy); @@ -1093,17 +1656,20 @@ export class Minimap extends ViewPart { backgroundColor: RGBA8, useLighterFont: boolean, renderMinimap: RenderMinimap, + charWidth: number, colorTracker: MinimapTokensColorTracker, minimapCharRenderer: MinimapCharRenderer, dy: number, + innerLinePadding: number, tabSize: number, lineData: ViewLineData, - fontScale: number + fontScale: number, + minimapLineHeight: number ): void { const content = lineData.content; const tokens = lineData.tokens; - const charWidth = getMinimapCharWidth(renderMinimap, fontScale); const maxDx = target.width - charWidth; + const force1pxHeight = (minimapLineHeight === 1); let dx = MINIMAP_GUTTER_WIDTH; let charIndex = 0; @@ -1135,9 +1701,9 @@ export class Minimap extends ViewPart { for (let i = 0; i < count; i++) { if (renderMinimap === RenderMinimap.Blocks) { - minimapCharRenderer.blockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont); + minimapCharRenderer.blockRenderChar(target, dx, dy + innerLinePadding, tokenColor, backgroundColor, useLighterFont, force1pxHeight); } else { // RenderMinimap.Text - minimapCharRenderer.renderChar(target, dx, dy, charCode, tokenColor, backgroundColor, fontScale, useLighterFont); + minimapCharRenderer.renderChar(target, dx, dy + innerLinePadding, charCode, tokenColor, backgroundColor, fontScale, useLighterFont, force1pxHeight); } dx += charWidth; diff --git a/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts b/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts index 6feeecc03ac7..c30b720c207a 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts @@ -34,11 +34,13 @@ export class MinimapCharRenderer { color: RGBA8, backgroundColor: RGBA8, fontScale: number, - useLighterFont: boolean + useLighterFont: boolean, + force1pxHeight: boolean ): void { const charWidth = Constants.BASE_CHAR_WIDTH * this.scale; const charHeight = Constants.BASE_CHAR_HEIGHT * this.scale; - if (dx + charWidth > target.width || dy + charHeight > target.height) { + const renderHeight = (force1pxHeight ? 1 : charHeight); + if (dx + charWidth > target.width || dy + renderHeight > target.height) { console.warn('bad render request outside image data'); return; } @@ -60,7 +62,7 @@ export class MinimapCharRenderer { let sourceOffset = charIndex * charWidth * charHeight; let row = dy * destWidth + dx * Constants.RGBA_CHANNELS_CNT; - for (let y = 0; y < charHeight; y++) { + for (let y = 0; y < renderHeight; y++) { let column = row; for (let x = 0; x < charWidth; x++) { const c = charData[sourceOffset++] / 255; @@ -80,11 +82,13 @@ export class MinimapCharRenderer { dy: number, color: RGBA8, backgroundColor: RGBA8, - useLighterFont: boolean + useLighterFont: boolean, + force1pxHeight: boolean ): void { const charWidth = Constants.BASE_CHAR_WIDTH * this.scale; const charHeight = Constants.BASE_CHAR_HEIGHT * this.scale; - if (dx + charWidth > target.width || dy + charHeight > target.height) { + const renderHeight = (force1pxHeight ? 1 : charHeight); + if (dx + charWidth > target.width || dy + renderHeight > target.height) { console.warn('bad render request outside image data'); return; } @@ -108,7 +112,7 @@ export class MinimapCharRenderer { const dest = target.data; let row = dy * destWidth + dx * Constants.RGBA_CHANNELS_CNT; - for (let y = 0; y < charHeight; y++) { + for (let y = 0; y < renderHeight; y++) { let column = row; for (let x = 0; x < charWidth; x++) { dest[column++] = colorR; diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts index 0cbac3c5035c..bd1aa10f8a8f 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts @@ -276,7 +276,10 @@ export class DecorationsOverviewRuler extends ViewPart { return true; } public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { - return true; + if (e.affectsOverviewRuler) { + return true; + } + return false; } public onFlushed(e: viewEvents.ViewFlushedEvent): boolean { return true; diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index ed8c20c1674b..7f488a873dc3 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -287,6 +287,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC public options!: ComputedEditorOptions; private _isDominatedByLongLines: boolean; + private _maxLineNumber: number; private _lineNumbersDigitCount: number; private _rawOptions: IEditorOptions; @@ -298,6 +299,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC this.isSimpleWidget = isSimpleWidget; this._isDominatedByLongLines = false; + this._maxLineNumber = 1; this._lineNumbersDigitCount = 1; this._rawOptions = deepCloneAndMigrateOptions(_options); @@ -347,6 +349,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC fontInfo: this.readConfiguration(bareFontInfo), extraEditorClassName: partialEnv.extraEditorClassName, isDominatedByLongLines: this._isDominatedByLongLines, + maxLineNumber: this._maxLineNumber, lineNumbersDigitCount: this._lineNumbersDigitCount, emptySelectionClipboard: partialEnv.emptySelectionClipboard, pixelRatio: partialEnv.pixelRatio, @@ -405,11 +408,11 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC } public setMaxLineNumber(maxLineNumber: number): void { - let digitCount = CommonEditorConfiguration._digitCount(maxLineNumber); - if (this._lineNumbersDigitCount === digitCount) { + if (this._maxLineNumber === maxLineNumber) { return; } - this._lineNumbersDigitCount = digitCount; + this._maxLineNumber = maxLineNumber; + this._lineNumbersDigitCount = CommonEditorConfiguration._digitCount(maxLineNumber); this._recomputeOptions(); } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 50d6dd6c3e0b..81949c640473 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -672,6 +672,7 @@ export interface IEnvironmentalOptions { readonly fontInfo: FontInfo; readonly extraEditorClassName: string; readonly isDominatedByLongLines: boolean; + readonly maxLineNumber: number; readonly lineNumbersDigitCount: number; readonly emptySelectionClipboard: boolean; readonly pixelRatio: number; @@ -1685,6 +1686,14 @@ export interface EditorLayoutInfo { * The width of the minimap */ readonly minimapWidth: number; + readonly minimapHeightIsEditorHeight: boolean; + readonly minimapIsSampling: boolean; + readonly minimapScale: number; + readonly minimapLineHeight: number; + readonly minimapCanvasInnerWidth: number; + readonly minimapCanvasInnerHeight: number; + readonly minimapCanvasOuterWidth: number; + readonly minimapCanvasOuterHeight: number; /** * Minimap render type @@ -1718,6 +1727,7 @@ export interface EditorLayoutInfoComputerEnv { outerWidth: number; outerHeight: number; lineHeight: number; + maxLineNumber: number; lineNumbersDigitCount: number; typicalHalfwidthCharacterWidth: number; maxDigitWidth: number; @@ -1741,6 +1751,7 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption= 2 ? Math.round(minimap.scale * 2) : minimap.scale); + let minimapScale = (pixelRatio >= 2 ? Math.round(minimap.scale * 2) : minimap.scale); const minimapMaxColumn = minimap.maxColumn | 0; + const minimapMode = minimap.mode; const scrollbar = options.get(EditorOption.scrollbar); const verticalScrollbarWidth = scrollbar.verticalScrollbarSize | 0; @@ -1805,19 +1832,65 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption 1) { + minimapHeightIsEditorHeight = true; + minimapIsSampling = true; + minimapScale = 1; + minimapLineHeight = 1; + minimapCharWidth = minimapScale / pixelRatio; + } else { + const effectiveMinimapHeight = Math.ceil((modelLineCount + extraLinesBeyondLastLine) * minimapLineHeight); + if (minimapMode === 'cover' || effectiveMinimapHeight > minimapCanvasInnerHeight) { + minimapHeightIsEditorHeight = true; + const configuredFontScale = minimapScale; + minimapLineHeight = Math.min(lineHeight * pixelRatio, Math.max(1, Math.floor(1 / desiredRatio))); + minimapScale = Math.min(configuredFontScale + 1, Math.max(1, Math.floor(minimapLineHeight / baseCharHeight))); + if (minimapScale > configuredFontScale) { + minimapWidthMultiplier = Math.min(2, minimapScale / configuredFontScale); + } + minimapCharWidth = minimapScale / pixelRatio / minimapWidthMultiplier; + minimapCanvasInnerHeight = Math.ceil((Math.max(typicalViewportLineCount, modelLineCount + extraLinesBeyondLastLine)) * minimapLineHeight); + } + } + } + renderMinimap = minimapRenderCharacters ? RenderMinimap.Text : RenderMinimap.Blocks; // Given: @@ -1849,6 +1922,10 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption(input.mode, this.defaultValue.mode, ['actual', 'cover', 'contain']), side: EditorStringEnumOption.stringSet<'right' | 'left'>(input.side, this.defaultValue.side, ['right', 'left']), showSlider: EditorStringEnumOption.stringSet<'always' | 'mouseover'>(input.showSlider, this.defaultValue.showSlider, ['always', 'mouseover']), renderCharacters: EditorBooleanOption.boolean(input.renderCharacters, this.defaultValue.renderCharacters), diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 2c1fcc8f2481..8aa9b5a53d48 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1423,19 +1423,15 @@ export class TextModel extends Disposable implements model.ITextModel { private _changeDecorations(ownerId: number, callback: (changeAccessor: model.IModelDecorationsChangeAccessor) => T): T | null { let changeAccessor: model.IModelDecorationsChangeAccessor = { addDecoration: (range: IRange, options: model.IModelDecorationOptions): string => { - this._onDidChangeDecorations.fire(); return this._deltaDecorationsImpl(ownerId, [], [{ range: range, options: options }])[0]; }, changeDecoration: (id: string, newRange: IRange): void => { - this._onDidChangeDecorations.fire(); this._changeDecorationImpl(id, newRange); }, changeDecorationOptions: (id: string, options: model.IModelDecorationOptions) => { - this._onDidChangeDecorations.fire(); this._changeDecorationOptionsImpl(id, _normalizeOptions(options)); }, removeDecoration: (id: string): void => { - this._onDidChangeDecorations.fire(); this._deltaDecorationsImpl(ownerId, [id], []); }, deltaDecorations: (oldDecorations: string[], newDecorations: model.IModelDeltaDecoration[]): string[] => { @@ -1443,7 +1439,6 @@ export class TextModel extends Disposable implements model.ITextModel { // nothing to do return []; } - this._onDidChangeDecorations.fire(); return this._deltaDecorationsImpl(ownerId, oldDecorations, newDecorations); } }; @@ -1474,7 +1469,6 @@ export class TextModel extends Disposable implements model.ITextModel { try { this._onDidChangeDecorations.beginDeferredEmit(); - this._onDidChangeDecorations.fire(); return this._deltaDecorationsImpl(ownerId, oldDecorations, newDecorations); } finally { this._onDidChangeDecorations.endDeferredEmit(); @@ -1622,6 +1616,7 @@ export class TextModel extends Disposable implements model.ITextModel { this._decorationsTree.delete(node); node.reset(this.getVersionId(), startOffset, endOffset, range); this._decorationsTree.insert(node); + this._onDidChangeDecorations.checkAffectedAndFire(node.options); } private _changeDecorationOptionsImpl(decorationId: string, options: ModelDecorationOptions): void { @@ -1633,6 +1628,9 @@ export class TextModel extends Disposable implements model.ITextModel { const nodeWasInOverviewRuler = (node.options.overviewRuler && node.options.overviewRuler.color ? true : false); const nodeIsInOverviewRuler = (options.overviewRuler && options.overviewRuler.color ? true : false); + this._onDidChangeDecorations.checkAffectedAndFire(node.options); + this._onDidChangeDecorations.checkAffectedAndFire(options); + if (nodeWasInOverviewRuler !== nodeIsInOverviewRuler) { // Delete + Insert due to an overview ruler status change this._decorationsTree.delete(node); @@ -1666,6 +1664,7 @@ export class TextModel extends Disposable implements model.ITextModel { // (2) remove the node from the tree (if it exists) if (node) { this._decorationsTree.delete(node); + this._onDidChangeDecorations.checkAffectedAndFire(node.options); } } @@ -1688,6 +1687,7 @@ export class TextModel extends Disposable implements model.ITextModel { node.ownerId = ownerId; node.reset(versionId, startOffset, endOffset, range); node.setOptions(options); + this._onDidChangeDecorations.checkAffectedAndFire(options); this._decorationsTree.insert(node); @@ -1713,7 +1713,7 @@ export class TextModel extends Disposable implements model.ITextModel { throw new Error('Illegal value for lineNumber'); } - this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), tokens); + this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), tokens, false); } public setTokens(tokens: MultilineTokens[]): void { @@ -1725,16 +1725,34 @@ export class TextModel extends Disposable implements model.ITextModel { for (let i = 0, len = tokens.length; i < len; i++) { const element = tokens[i]; - ranges.push({ fromLineNumber: element.startLineNumber, toLineNumber: element.startLineNumber + element.tokens.length - 1 }); + let minChangedLineNumber = 0; + let maxChangedLineNumber = 0; + let hasChange = false; for (let j = 0, lenJ = element.tokens.length; j < lenJ; j++) { - this.setLineTokens(element.startLineNumber + j, element.tokens[j]); + const lineNumber = element.startLineNumber + j; + if (hasChange) { + this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.tokens[j], false); + maxChangedLineNumber = lineNumber; + } else { + const lineHasChange = this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.tokens[j], true); + if (lineHasChange) { + hasChange = true; + minChangedLineNumber = lineNumber; + maxChangedLineNumber = lineNumber; + } + } + } + if (hasChange) { + ranges.push({ fromLineNumber: minChangedLineNumber, toLineNumber: maxChangedLineNumber }); } } - this._emitModelTokensChangedEvent({ - tokenizationSupportChanged: false, - ranges: ranges - }); + if (ranges.length > 0) { + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: false, + ranges: ranges + }); + } } public setSemanticTokens(tokens: MultilineTokens2[] | null): void { @@ -3083,11 +3101,15 @@ export class DidChangeDecorationsEmitter extends Disposable { private _deferredCnt: number; private _shouldFire: boolean; + private _affectsMinimap: boolean; + private _affectsOverviewRuler: boolean; constructor() { super(); this._deferredCnt = 0; this._shouldFire = false; + this._affectsMinimap = false; + this._affectsOverviewRuler = false; } public beginDeferredEmit(): void { @@ -3098,13 +3120,31 @@ export class DidChangeDecorationsEmitter extends Disposable { this._deferredCnt--; if (this._deferredCnt === 0) { if (this._shouldFire) { + const event: IModelDecorationsChangedEvent = { + affectsMinimap: this._affectsMinimap, + affectsOverviewRuler: this._affectsOverviewRuler, + }; this._shouldFire = false; - this._actual.fire({}); + this._affectsMinimap = false; + this._affectsOverviewRuler = false; + this._actual.fire(event); } } } + public checkAffectedAndFire(options: ModelDecorationOptions): void { + if (!this._affectsMinimap) { + this._affectsMinimap = options.minimap && options.minimap.position ? true : false; + } + if (!this._affectsOverviewRuler) { + this._affectsOverviewRuler = options.overviewRuler && options.overviewRuler.color ? true : false; + } + this._shouldFire = true; + } + public fire(): void { + this._affectsMinimap = true; + this._affectsOverviewRuler = true; this._shouldFire = true; } } diff --git a/src/vs/editor/common/model/textModelEvents.ts b/src/vs/editor/common/model/textModelEvents.ts index 11fc0e4165f6..a09a1abf0fe4 100644 --- a/src/vs/editor/common/model/textModelEvents.ts +++ b/src/vs/editor/common/model/textModelEvents.ts @@ -76,6 +76,8 @@ export interface IModelContentChangedEvent { * An event describing that model decorations have changed. */ export interface IModelDecorationsChangedEvent { + readonly affectsMinimap: boolean; + readonly affectsOverviewRuler: boolean; } /** diff --git a/src/vs/editor/common/model/tokensStore.ts b/src/vs/editor/common/model/tokensStore.ts index e396aa054a2c..c662f820c9ca 100644 --- a/src/vs/editor/common/model/tokensStore.ts +++ b/src/vs/editor/common/model/tokensStore.ts @@ -964,10 +964,35 @@ export class TokensStore { this._len += insertCount; } - public setTokens(topLevelLanguageId: LanguageId, lineIndex: number, lineTextLength: number, _tokens: Uint32Array | ArrayBuffer | null): void { + public setTokens(topLevelLanguageId: LanguageId, lineIndex: number, lineTextLength: number, _tokens: Uint32Array | ArrayBuffer | null, checkEquality: boolean): boolean { const tokens = TokensStore._massageTokens(topLevelLanguageId, lineTextLength, _tokens); this._ensureLine(lineIndex); + const oldTokens = this._lineTokens[lineIndex]; this._lineTokens[lineIndex] = tokens; + + if (checkEquality) { + return !TokensStore._equals(oldTokens, tokens); + } + return false; + } + + private static _equals(_a: Uint32Array | ArrayBuffer | null, _b: Uint32Array | ArrayBuffer | null) { + if (!_a || !_b) { + return !_a && !_b; + } + + const a = toUint32Array(_a); + const b = toUint32Array(_b); + + if (a.length !== b.length) { + return false; + } + for (let i = 0, len = a.length; i < len; i++) { + if (a[i] !== b[i]) { + return false; + } + } + return true; } //#region Editing diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 7d925b4bc2c0..0340900a5c44 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -265,6 +265,34 @@ export interface HoverProvider { provideHover(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; } +/** + * An evaluatable expression represents additional information for an expression in a document. Evaluatable expression are + * evaluated by a debugger or runtime and their result is rendered in a tooltip-like widget. + */ +export interface EvaluatableExpression { + /** + * The range to which this expression applies. + */ + range: IRange; + /* + * This expression overrides the expression extracted from the range. + */ + expression?: string; +} + +/** + * The hover provider interface defines the contract between extensions and + * the [hover](https://code.visualstudio.com/docs/editor/intellisense)-feature. + */ +export interface EvaluatableExpressionProvider { + /** + * Provide a hover for the given position and document. Multiple hovers at the same + * position will be merged by the editor. A hover can have a range which defaults + * to the word range at the position when omitted. + */ + provideEvaluatableExpression(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; +} + export const enum CompletionItemKind { Method, Function, @@ -1595,6 +1623,11 @@ export const SignatureHelpProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const EvaluatableExpressionProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ diff --git a/src/vs/editor/common/view/viewEvents.ts b/src/vs/editor/common/view/viewEvents.ts index 1d9290b5e764..0f2720d8514b 100644 --- a/src/vs/editor/common/view/viewEvents.ts +++ b/src/vs/editor/common/view/viewEvents.ts @@ -10,6 +10,7 @@ import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ScrollType, IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; +import { IModelDecorationsChangedEvent } from 'vs/editor/common/model/textModelEvents'; export const enum ViewEventType { ViewConfigurationChanged = 1, @@ -82,8 +83,17 @@ export class ViewDecorationsChangedEvent { public readonly type = ViewEventType.ViewDecorationsChanged; - constructor() { - // Nothing to do + readonly affectsMinimap: boolean; + readonly affectsOverviewRuler: boolean; + + constructor(source: IModelDecorationsChangedEvent | null) { + if (source) { + this.affectsMinimap = source.affectsMinimap; + this.affectsOverviewRuler = source.affectsOverviewRuler; + } else { + this.affectsMinimap = true; + this.affectsOverviewRuler = true; + } } } diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index ed3b33825357..171a4839949e 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -172,7 +172,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel if (this.lines.setWrappingSettings(fontInfo, wrappingStrategy, wrappingInfo.wrappingColumn, wrappingIndent)) { eventsCollector.emit(new viewEvents.ViewFlushedEvent()); eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); + eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); this.decorations.onLineMappingChanged(); this.viewLayout.onFlushed(this.getLineCount()); @@ -185,7 +185,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel if (e.hasChanged(EditorOption.readOnly)) { // Must read again all decorations due to readOnly filtering this.decorations.reset(); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); + eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); } eventsCollector.emit(new viewEvents.ViewConfigurationChangedEvent(e)); @@ -291,7 +291,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel if (!hadOtherModelChange && hadModelLineChangeThatChangedLineMapping) { eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); + eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); this.decorations.onLineMappingChanged(); } } finally { @@ -354,7 +354,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel const eventsCollector = this._beginEmit(); eventsCollector.emit(new viewEvents.ViewFlushedEvent()); eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); + eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); } finally { this._endEmit(); } @@ -365,7 +365,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel this.decorations.onModelDecorationsChanged(); try { const eventsCollector = this._beginEmit(); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); + eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(e)); } finally { this._endEmit(); } @@ -379,7 +379,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel if (lineMappingChanged) { eventsCollector.emit(new viewEvents.ViewFlushedEvent()); eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); + eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); this.decorations.onLineMappingChanged(); this.viewLayout.onFlushed(this.getLineCount()); this.viewLayout.onHeightMaybeChanged(); diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 96c7b3a0d17b..440c78fef727 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -419,8 +419,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } if (currentMatch) { const ariaLabel = nls.localize('ariaSearchNoResultWithLineNum', "{0} found for '{1}', at {2}", label, searchString, currentMatch.startLineNumber + ':' + currentMatch.startColumn); - const lineContent = this._codeEditor.getModel()?.getLineContent(currentMatch.startLineNumber); - if (lineContent) { + const model = this._codeEditor.getModel(); + if (model && (currentMatch.startLineNumber <= model.getLineCount()) && (currentMatch.startLineNumber >= 1)) { + const lineContent = model.getLineContent(currentMatch.startLineNumber); return `${lineContent}, ${ariaLabel}`; } diff --git a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts index b0b0762997c4..cde867901417 100644 --- a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts +++ b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts @@ -78,7 +78,7 @@ suite('MinimapCharRenderer', () => { imageData.data[4 * i + 2] = background.b; imageData.data[4 * i + 3] = 255; } - renderer.renderChar(imageData, 0, 0, 'd'.charCodeAt(0), color, background, 2, false); + renderer.renderChar(imageData, 0, 0, 'd'.charCodeAt(0), color, background, 2, false, false); let actual: number[] = []; for (let i = 0; i < imageData.data.length; i++) { @@ -108,7 +108,7 @@ suite('MinimapCharRenderer', () => { imageData.data[4 * i + 3] = 255; } - renderer.renderChar(imageData, 0, 0, 'd'.charCodeAt(0), color, background, 1, false); + renderer.renderChar(imageData, 0, 0, 'd'.charCodeAt(0), color, background, 1, false, false); let actual: number[] = []; for (let i = 0; i < imageData.data.length; i++) { diff --git a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts index cfb8396fe377..f3c94bca5e32 100644 --- a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts @@ -17,6 +17,7 @@ interface IEditorLayoutProviderOpts { readonly showLineNumbers: boolean; readonly lineNumbersMinChars: number; readonly lineNumbersDigitCount: number; + maxLineNumber?: number; readonly lineDecorationsWidth: number; @@ -32,6 +33,7 @@ interface IEditorLayoutProviderOpts { readonly minimapSide: 'left' | 'right'; readonly minimapRenderCharacters: boolean; readonly minimapMaxColumn: number; + minimapMode?: 'actual' | 'cover' | 'contain'; readonly pixelRatio: number; } @@ -45,6 +47,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { options._write(EditorOption.folding, false); const minimapOptions: EditorMinimapOptions = { enabled: input.minimap, + mode: input.minimapMode || 'actual', side: input.minimapSide, renderCharacters: input.minimapRenderCharacters, maxColumn: input.minimapMaxColumn, @@ -77,6 +80,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { outerWidth: input.outerWidth, outerHeight: input.outerHeight, lineHeight: input.lineHeight, + maxLineNumber: input.maxLineNumber || Math.pow(10, input.lineNumbersDigitCount) - 1, lineNumbersDigitCount: input.lineNumbersDigitCount, typicalHalfwidthCharacterWidth: input.typicalHalfwidthCharacterWidth, maxDigitWidth: input.maxDigitWidth, @@ -125,6 +129,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 800, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 800, viewportColumn: 98, verticalScrollbarWidth: 0, @@ -179,6 +191,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 800, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 800, viewportColumn: 97, verticalScrollbarWidth: 11, @@ -233,6 +253,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 800, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 800, viewportColumn: 88, verticalScrollbarWidth: 0, @@ -287,6 +315,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 88, verticalScrollbarWidth: 0, @@ -341,6 +377,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 88, verticalScrollbarWidth: 0, @@ -395,6 +439,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 83, verticalScrollbarWidth: 0, @@ -449,6 +501,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 83, verticalScrollbarWidth: 0, @@ -503,6 +563,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 82, verticalScrollbarWidth: 0, @@ -557,6 +625,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 171, verticalScrollbarWidth: 0, @@ -611,6 +687,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 169, verticalScrollbarWidth: 0, @@ -665,6 +749,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.Text, minimapLeft: 903, minimapWidth: 97, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 2, + minimapCanvasInnerWidth: 97, + minimapCanvasInnerHeight: 800, + minimapCanvasOuterWidth: 97, + minimapCanvasOuterHeight: 800, viewportColumn: 89, verticalScrollbarWidth: 0, @@ -719,6 +811,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.Text, minimapLeft: 903, minimapWidth: 97, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 194, + minimapCanvasInnerHeight: 1600, + minimapCanvasOuterWidth: 97, + minimapCanvasOuterHeight: 800, viewportColumn: 89, verticalScrollbarWidth: 0, @@ -773,6 +873,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.Text, minimapLeft: 945, minimapWidth: 55, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 220, + minimapCanvasInnerHeight: 3200, + minimapCanvasOuterWidth: 55, + minimapCanvasOuterHeight: 800, viewportColumn: 93, verticalScrollbarWidth: 0, @@ -827,6 +935,270 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.Text, minimapLeft: 0, minimapWidth: 55, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 220, + minimapCanvasInnerHeight: 3200, + minimapCanvasOuterWidth: 55, + minimapCanvasOuterHeight: 800, + viewportColumn: 93, + + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + + overviewRuler: { + top: 0, + width: 0, + height: 800, + right: 0 + } + }); + }); + + test('EditorLayoutProvider 11 - minimap mode cover without sampling', () => { + doTest({ + outerWidth: 1000, + outerHeight: 800, + showGlyphMargin: false, + lineHeight: 16, + showLineNumbers: false, + lineNumbersMinChars: 0, + lineNumbersDigitCount: 3, + maxLineNumber: 120, + lineDecorationsWidth: 10, + typicalHalfwidthCharacterWidth: 10, + maxDigitWidth: 10, + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + scrollbarArrowSize: 0, + verticalScrollbarHasArrows: false, + minimap: true, + minimapSide: 'right', + minimapRenderCharacters: true, + minimapMaxColumn: 150, + minimapMode: 'cover', + pixelRatio: 2, + }, { + width: 1000, + height: 800, + + glyphMarginLeft: 0, + glyphMarginWidth: 0, + + lineNumbersLeft: 0, + lineNumbersWidth: 0, + + decorationsLeft: 0, + decorationsWidth: 10, + + contentLeft: 10, + contentWidth: 893, + + renderMinimap: RenderMinimap.Text, + minimapLeft: 903, + minimapWidth: 97, + minimapHeightIsEditorHeight: true, + minimapIsSampling: false, + minimapScale: 3, + minimapLineHeight: 13, + minimapCanvasInnerWidth: 291, + minimapCanvasInnerHeight: 1560, + minimapCanvasOuterWidth: 97, + minimapCanvasOuterHeight: 800, + viewportColumn: 89, + + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + + overviewRuler: { + top: 0, + width: 0, + height: 800, + right: 0 + } + }); + }); + + test('EditorLayoutProvider 12 - minimap mode cover with sampling', () => { + doTest({ + outerWidth: 1000, + outerHeight: 800, + showGlyphMargin: false, + lineHeight: 16, + showLineNumbers: false, + lineNumbersMinChars: 0, + lineNumbersDigitCount: 4, + maxLineNumber: 2500, + lineDecorationsWidth: 10, + typicalHalfwidthCharacterWidth: 10, + maxDigitWidth: 10, + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + scrollbarArrowSize: 0, + verticalScrollbarHasArrows: false, + minimap: true, + minimapSide: 'right', + minimapRenderCharacters: true, + minimapMaxColumn: 150, + minimapMode: 'cover', + pixelRatio: 2, + }, { + width: 1000, + height: 800, + + glyphMarginLeft: 0, + glyphMarginWidth: 0, + + lineNumbersLeft: 0, + lineNumbersWidth: 0, + + decorationsLeft: 0, + decorationsWidth: 10, + + contentLeft: 10, + contentWidth: 935, + + renderMinimap: RenderMinimap.Text, + minimapLeft: 945, + minimapWidth: 55, + minimapHeightIsEditorHeight: true, + minimapIsSampling: true, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 110, + minimapCanvasInnerHeight: 1600, + minimapCanvasOuterWidth: 55, + minimapCanvasOuterHeight: 800, + viewportColumn: 93, + + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + + overviewRuler: { + top: 0, + width: 0, + height: 800, + right: 0 + } + }); + }); + + test('EditorLayoutProvider 13 - minimap mode contain without sampling', () => { + doTest({ + outerWidth: 1000, + outerHeight: 800, + showGlyphMargin: false, + lineHeight: 16, + showLineNumbers: false, + lineNumbersMinChars: 0, + lineNumbersDigitCount: 3, + maxLineNumber: 120, + lineDecorationsWidth: 10, + typicalHalfwidthCharacterWidth: 10, + maxDigitWidth: 10, + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + scrollbarArrowSize: 0, + verticalScrollbarHasArrows: false, + minimap: true, + minimapSide: 'right', + minimapRenderCharacters: true, + minimapMaxColumn: 150, + minimapMode: 'contain', + pixelRatio: 2, + }, { + width: 1000, + height: 800, + + glyphMarginLeft: 0, + glyphMarginWidth: 0, + + lineNumbersLeft: 0, + lineNumbersWidth: 0, + + decorationsLeft: 0, + decorationsWidth: 10, + + contentLeft: 10, + contentWidth: 893, + + renderMinimap: RenderMinimap.Text, + minimapLeft: 903, + minimapWidth: 97, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 194, + minimapCanvasInnerHeight: 1600, + minimapCanvasOuterWidth: 97, + minimapCanvasOuterHeight: 800, + viewportColumn: 89, + + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + + overviewRuler: { + top: 0, + width: 0, + height: 800, + right: 0 + } + }); + }); + + test('EditorLayoutProvider 14 - minimap mode contain with sampling', () => { + doTest({ + outerWidth: 1000, + outerHeight: 800, + showGlyphMargin: false, + lineHeight: 16, + showLineNumbers: false, + lineNumbersMinChars: 0, + lineNumbersDigitCount: 4, + maxLineNumber: 2500, + lineDecorationsWidth: 10, + typicalHalfwidthCharacterWidth: 10, + maxDigitWidth: 10, + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + scrollbarArrowSize: 0, + verticalScrollbarHasArrows: false, + minimap: true, + minimapSide: 'right', + minimapRenderCharacters: true, + minimapMaxColumn: 150, + minimapMode: 'contain', + pixelRatio: 2, + }, { + width: 1000, + height: 800, + + glyphMarginLeft: 0, + glyphMarginWidth: 0, + + lineNumbersLeft: 0, + lineNumbersWidth: 0, + + decorationsLeft: 0, + decorationsWidth: 10, + + contentLeft: 10, + contentWidth: 935, + + renderMinimap: RenderMinimap.Text, + minimapLeft: 945, + minimapWidth: 55, + minimapHeightIsEditorHeight: true, + minimapIsSampling: true, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 110, + minimapCanvasInnerHeight: 1600, + minimapCanvasOuterWidth: 55, + minimapCanvasOuterHeight: 800, viewportColumn: 93, verticalScrollbarWidth: 0, @@ -881,6 +1253,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.Text, minimapLeft: 1096, minimapWidth: 91, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 182, + minimapCanvasInnerHeight: 844, + minimapCanvasOuterWidth: 91, + minimapCanvasOuterHeight: 422, viewportColumn: 83, verticalScrollbarWidth: 14, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index ff031806fb3f..73a615757dc4 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2390,6 +2390,8 @@ declare namespace monaco.editor { * An event describing that model decorations have changed. */ export interface IModelDecorationsChangedEvent { + readonly affectsMinimap: boolean; + readonly affectsOverviewRuler: boolean; } export interface IModelOptionsChangedEvent { @@ -3330,6 +3332,14 @@ declare namespace monaco.editor { * The width of the minimap */ readonly minimapWidth: number; + readonly minimapHeightIsEditorHeight: boolean; + readonly minimapIsSampling: boolean; + readonly minimapScale: number; + readonly minimapLineHeight: number; + readonly minimapCanvasInnerWidth: number; + readonly minimapCanvasInnerHeight: number; + readonly minimapCanvasOuterWidth: number; + readonly minimapCanvasOuterHeight: number; /** * Minimap render type */ @@ -3379,6 +3389,11 @@ declare namespace monaco.editor { * Defaults to 'right'. */ side?: 'right' | 'left'; + /** + * Control the minimap rendering mode. + * Defaults to 'actual'. + */ + mode?: 'actual' | 'cover' | 'contain'; /** * Control the rendering of the minimap slider. * Defaults to 'mouseover'. @@ -5238,6 +5253,31 @@ declare namespace monaco.languages { provideHover(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; } + /** + * An evaluatable expression represents additional information for an expression in a document. Evaluatable expression are + * evaluated by a debugger or runtime and their result is rendered in a tooltip-like widget. + */ + export interface EvaluatableExpression { + /** + * The range to which this expression applies. + */ + range: IRange; + expression?: string; + } + + /** + * The hover provider interface defines the contract between extensions and + * the [hover](https://code.visualstudio.com/docs/editor/intellisense)-feature. + */ + export interface EvaluatableExpressionProvider { + /** + * Provide a hover for the given position and document. Multiple hovers at the same + * position will be merged by the editor. A hover can have a range which defaults + * to the word range at the position when omitted. + */ + provideEvaluatableExpression(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; + } + export enum CompletionItemKind { Method = 0, Function = 1, diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 25a4db0972c4..c30c37e13943 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -114,7 +114,9 @@ export class MenuId { static readonly CommentActions = new MenuId('CommentActions'); static readonly BulkEditTitle = new MenuId('BulkEditTitle'); static readonly BulkEditContext = new MenuId('BulkEditContext'); - + static readonly TimelineItemContext = new MenuId('TimelineItemContext'); + static readonly TimelineTitle = new MenuId('TimelineTitle'); + static readonly TimelineTitleContext = new MenuId('TimelineTitleContext'); readonly id: number; readonly _debugName: string; diff --git a/src/vs/platform/instantiation/common/instantiation.ts b/src/vs/platform/instantiation/common/instantiation.ts index 10ff5a25051d..7e130cc39632 100644 --- a/src/vs/platform/instantiation/common/instantiation.ts +++ b/src/vs/platform/instantiation/common/instantiation.ts @@ -29,7 +29,7 @@ export interface IConstructorSignature0 { } export interface IConstructorSignature1 { - new(first: A1, ...services: BrandedService[]): T; + new (first: A1, ...services: Services): T; } export interface IConstructorSignature2 { diff --git a/src/vs/platform/markers/common/markers.ts b/src/vs/platform/markers/common/markers.ts index 0d852a63de83..e89a52153d2f 100644 --- a/src/vs/platform/markers/common/markers.ts +++ b/src/vs/platform/markers/common/markers.ts @@ -135,15 +135,15 @@ export namespace IMarkerData { export function makeKeyOptionalMessage(markerData: IMarkerData, useMessage: boolean): string { let result: string[] = [emptyString]; if (markerData.source) { - result.push(markerData.source.replace('¦', '\¦')); + result.push(markerData.source.replace('¦', '\\¦')); } else { result.push(emptyString); } if (markerData.code) { if (typeof markerData.code === 'string') { - result.push(markerData.code.replace('¦', '\¦')); + result.push(markerData.code.replace('¦', '\\¦')); } else { - result.push(markerData.code.value.replace('¦', '\¦')); + result.push(markerData.code.value.replace('¦', '\\¦')); } } else { result.push(emptyString); @@ -157,7 +157,7 @@ export namespace IMarkerData { // Modifed to not include the message as part of the marker key to work around // https://github.com/microsoft/vscode/issues/77475 if (markerData.message && useMessage) { - result.push(markerData.message.replace('¦', '\¦')); + result.push(markerData.message.replace('¦', '\\¦')); } else { result.push(emptyString); } diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index 93429296cfed..aff1675b214c 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -6,7 +6,7 @@ import BaseSeverity from 'vs/base/common/severity'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IAction } from 'vs/base/common/actions'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; export import Severity = BaseSeverity; @@ -318,16 +318,14 @@ export class NoOpNotification implements INotificationHandle { readonly progress = new NoOpProgress(); - private readonly _onDidClose: Emitter = new Emitter(); - readonly onDidClose: Event = this._onDidClose.event; + readonly onDidClose = Event.None; + readonly onDidChangeVisibility = Event.None; updateSeverity(severity: Severity): void { } updateMessage(message: NotificationMessage): void { } updateActions(actions?: INotificationActions): void { } - close(): void { - this._onDidClose.dispose(); - } + close(): void { } } export class NoOpProgress implements INotificationProgress { diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 2b7e3b8b3856..576ae6d82fb6 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -17,7 +17,11 @@ export interface IProgressService { _serviceBrand: undefined; - withProgress(options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, task: (progress: IProgress) => Promise, onDidCancel?: (choice?: number) => void): Promise; + withProgress( + options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, + task: (progress: IProgress) => Promise, + onDidCancel?: (choice?: number) => void + ): Promise; } export interface IProgressIndicator { @@ -45,19 +49,19 @@ export const enum ProgressLocation { } export interface IProgressOptions { - location: ProgressLocation | string; - title?: string; - source?: string; - total?: number; - cancellable?: boolean; - buttons?: string[]; + readonly location: ProgressLocation | string; + readonly title?: string; + readonly source?: string; + readonly total?: number; + readonly cancellable?: boolean; + readonly buttons?: string[]; } export interface IProgressNotificationOptions extends IProgressOptions { readonly location: ProgressLocation.Notification; readonly primaryActions?: ReadonlyArray; readonly secondaryActions?: ReadonlyArray; - delay?: number; + readonly delay?: number; } export interface IProgressWindowOptions extends IProgressOptions { @@ -66,8 +70,8 @@ export interface IProgressWindowOptions extends IProgressOptions { } export interface IProgressCompositeOptions extends IProgressOptions { - location: ProgressLocation.Explorer | ProgressLocation.Extensions | ProgressLocation.Scm | string; - delay?: number; + readonly location: ProgressLocation.Explorer | ProgressLocation.Extensions | ProgressLocation.Scm | string; + readonly delay?: number; } export interface IProgressStep { @@ -96,20 +100,14 @@ export interface IProgress { export class Progress implements IProgress { - private _callback: (data: T) => void; private _value?: T; + get value(): T | undefined { return this._value; } - constructor(callback: (data: T) => void) { - this._callback = callback; - } - - get value(): T | undefined { - return this._value; - } + constructor(private callback: (data: T) => void) { } report(item: T) { this._value = item; - this._callback(this._value); + this.callback(this._value); } } diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 7ebe69a2c9c7..e4d1c0ae4f8b 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -98,7 +98,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto async triggerAutoSync(): Promise { if (this.enabled) { return this.syncDelayer.trigger(() => { - this.logService.info('Auto Sync: Triggerred.'); + this.logService.info('Auto Sync: Triggered.'); return this.sync(false, true); }, this.successiveFailures ? 1000 * 1 * Math.min(this.successiveFailures, 60) /* Delay by number of seconds as number of failures up to 1 minute */ diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index a782d93c1a65..637dff308173 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -876,7 +876,65 @@ declare module 'vscode' { //#endregion - //#region Debug: + //#region locate evaluatable expressions for debug hover: https://github.com/microsoft/vscode/issues/89084 + + /** + * An EvaluatableExpression represents an expression in a document that can be evaluated by an active debugger or runtime. + * The result of this evaluation is shown in a tooltip-like widget. + * If only a range is specified, the expression will be extracted from the underlying document. + * An optional expression can be used to override the extracted expression. + * In this case the range is still used to highlight the range in the document. + */ + export class EvaluatableExpression { + /* + * The range is used to extract the evaluatable expression from the underlying document and to highlight it. + */ + readonly range: Range; + /* + * If specified the expression overrides the extracted expression. + */ + readonly expression?: string; + + /** + * Creates a new evaluatable expression object. + * + * @param range The range in the underlying document from which the evaluatable expression is extracted. + * @param expression If specified overrides the extracted expression. + */ + constructor(range: Range, expression?: string); + } + + /** + * The evaluatable expression provider interface defines the contract between extensions and + * the debug hover. + */ + export interface EvaluatableExpressionProvider { + + /** + * Provide an evaluatable expression for the given document and position. + * The expression can be implicitly specified by the range in the underlying document or by explicitly returning an expression. + * + * @param document The document in which the command was invoked. + * @param position The position where the command was invoked. + * @param token A cancellation token. + * @return An EvaluatableExpression or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined` or `null`. + */ + provideEvaluatableExpression(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + } + + export namespace languages { + /** + * Register a provider that locates evaluatable expressions in text documents. + * + * If multiple providers are registered for a language an arbitrary provider will be used. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An evaluatable expression provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerEvaluatableExpressionProvider(selector: DocumentSelector, provider: EvaluatableExpressionProvider): Disposable; + } // deprecated @@ -1553,6 +1611,40 @@ declare module 'vscode' { uri?: Uri; } + export interface TimelineCursor { + /** + * A provider-defined cursor specifing the range of timeline items to be returned. Must be serializable. + */ + cursor?: any; + + /** + * A flag to specify whether the timeline items requested are before or after (default) the provided cursor. + */ + before?: boolean; + + /** + * The maximum number of timeline items that should be returned. + */ + limit?: number; + } + + export interface Timeline { + /** + * A provider-defined cursor specifing the range of timeline items returned. Must be serializable. + */ + cursor?: any; + + /** + * A flag which indicates whether there are any more items that weren't returned. + */ + more?: boolean; + + /** + * An array of [timeline items](#TimelineItem). + */ + items: TimelineItem[]; + } + export interface TimelineProvider { /** * An optional event to signal that the timeline for a source has changed. @@ -1575,10 +1667,11 @@ declare module 'vscode' { * * @param uri The [uri](#Uri) of the file to provide the timeline for. * @param token A cancellation token. - * @return An array of timeline items or a thenable that resolves to such. The lack of a result + * @param cursor TBD + * @return The [timeline result](#TimelineResult) or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ - provideTimeline(uri: Uri, token: CancellationToken): ProviderResult; + provideTimeline(uri: Uri, cursor: TimelineCursor, token: CancellationToken): ProviderResult; } export namespace workspace { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 678a44ca3f3e..de204a88dbda 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -213,6 +213,16 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha })); } + // --- debug hover + + $registerEvaluatableExpressionProvider(handle: number, selector: IDocumentFilterDto[]): void { + this._registrations.set(handle, modes.EvaluatableExpressionProviderRegistry.register(selector, { + provideEvaluatableExpression: (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise => { + return this._proxy.$provideEvaluatableExpression(handle, model.uri, position, token); + } + })); + } + // --- occurrences $registerDocumentHighlightProvider(handle: number, selector: IDocumentFilterDto[]): void { diff --git a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts index 288a349b183b..ce7aa7a7b536 100644 --- a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts @@ -3,304 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IdleValue, raceCancellation } from 'vs/base/common/async'; -import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; -import * as strings from 'vs/base/common/strings'; -import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; -import { EditOperation } from 'vs/editor/common/core/editOperation'; -import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; -import { ITextModel } from 'vs/editor/common/model'; -import { CodeAction, CodeActionTriggerType } from 'vs/editor/common/modes'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService'; -import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; -import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; -import { CodeActionKind } from 'vs/editor/contrib/codeAction/types'; -import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; -import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { localize } from 'vs/nls'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IProgressService, ProgressLocation, IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; +import { IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { ISaveParticipant, IResolvedTextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileSaveParticipant, IResolvedTextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { SaveReason } from 'vs/workbench/common/editor'; import { ExtHostContext, ExtHostDocumentSaveParticipantShape, IExtHostContext } from '../common/extHost.protocol'; -import { ILabelService } from 'vs/platform/label/common/label'; import { canceled } from 'vs/base/common/errors'; +import { IDisposable } from 'vs/base/common/lifecycle'; -export interface ISaveParticipantParticipant { - participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason }, progress: IProgress, token: CancellationToken): Promise; -} - -class TrimWhitespaceParticipant implements ISaveParticipantParticipant { - - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService - ) { - // Nothing - } - - async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { - if (this.configurationService.getValue('files.trimTrailingWhitespace', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { - this.doTrimTrailingWhitespace(model.textEditorModel, env.reason === SaveReason.AUTO); - } - } - - private doTrimTrailingWhitespace(model: ITextModel, isAutoSaved: boolean): void { - let prevSelection: Selection[] = []; - let cursors: Position[] = []; - - const editor = findEditor(model, this.codeEditorService); - if (editor) { - // Find `prevSelection` in any case do ensure a good undo stack when pushing the edit - // Collect active cursors in `cursors` only if `isAutoSaved` to avoid having the cursors jump - prevSelection = editor.getSelections(); - if (isAutoSaved) { - cursors = prevSelection.map(s => s.getPosition()); - const snippetsRange = SnippetController2.get(editor).getSessionEnclosingRange(); - if (snippetsRange) { - for (let lineNumber = snippetsRange.startLineNumber; lineNumber <= snippetsRange.endLineNumber; lineNumber++) { - cursors.push(new Position(lineNumber, model.getLineMaxColumn(lineNumber))); - } - } - } - } - - const ops = trimTrailingWhitespace(model, cursors); - if (!ops.length) { - return; // Nothing to do - } - - model.pushEditOperations(prevSelection, ops, (_edits) => prevSelection); - } -} - -function findEditor(model: ITextModel, codeEditorService: ICodeEditorService): IActiveCodeEditor | null { - let candidate: IActiveCodeEditor | null = null; - - if (model.isAttachedToEditor()) { - for (const editor of codeEditorService.listCodeEditors()) { - if (editor.hasModel() && editor.getModel() === model) { - if (editor.hasTextFocus()) { - return editor; // favour focused editor if there are multiple - } - - candidate = editor; - } - } - } - - return candidate; -} - -export class FinalNewLineParticipant implements ISaveParticipantParticipant { - - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService - ) { - // Nothing - } - - async participate(model: IResolvedTextFileEditorModel, _env: { reason: SaveReason; }): Promise { - if (this.configurationService.getValue('files.insertFinalNewline', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { - this.doInsertFinalNewLine(model.textEditorModel); - } - } - - private doInsertFinalNewLine(model: ITextModel): void { - const lineCount = model.getLineCount(); - const lastLine = model.getLineContent(lineCount); - const lastLineIsEmptyOrWhitespace = strings.lastNonWhitespaceIndex(lastLine) === -1; - - if (!lineCount || lastLineIsEmptyOrWhitespace) { - return; - } - - const edits = [EditOperation.insert(new Position(lineCount, model.getLineMaxColumn(lineCount)), model.getEOL())]; - const editor = findEditor(model, this.codeEditorService); - if (editor) { - editor.executeEdits('insertFinalNewLine', edits, editor.getSelections()); - } else { - model.pushEditOperations([], edits, () => null); - } - } -} - -export class TrimFinalNewLinesParticipant implements ISaveParticipantParticipant { - - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService - ) { - // Nothing - } - - async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { - if (this.configurationService.getValue('files.trimFinalNewlines', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { - this.doTrimFinalNewLines(model.textEditorModel, env.reason === SaveReason.AUTO); - } - } - - /** - * returns 0 if the entire file is empty or whitespace only - */ - private findLastLineWithContent(model: ITextModel): number { - for (let lineNumber = model.getLineCount(); lineNumber >= 1; lineNumber--) { - const lineContent = model.getLineContent(lineNumber); - if (strings.lastNonWhitespaceIndex(lineContent) !== -1) { - // this line has content - return lineNumber; - } - } - // no line has content - return 0; - } - - private doTrimFinalNewLines(model: ITextModel, isAutoSaved: boolean): void { - const lineCount = model.getLineCount(); - - // Do not insert new line if file does not end with new line - if (lineCount === 1) { - return; - } - - let prevSelection: Selection[] = []; - let cannotTouchLineNumber = 0; - const editor = findEditor(model, this.codeEditorService); - if (editor) { - prevSelection = editor.getSelections(); - if (isAutoSaved) { - for (let i = 0, len = prevSelection.length; i < len; i++) { - const positionLineNumber = prevSelection[i].positionLineNumber; - if (positionLineNumber > cannotTouchLineNumber) { - cannotTouchLineNumber = positionLineNumber; - } - } - } - } - - const lastLineNumberWithContent = this.findLastLineWithContent(model); - const deleteFromLineNumber = Math.max(lastLineNumberWithContent + 1, cannotTouchLineNumber + 1); - const deletionRange = model.validateRange(new Range(deleteFromLineNumber, 1, lineCount, model.getLineMaxColumn(lineCount))); - - if (deletionRange.isEmpty()) { - return; - } - - model.pushEditOperations(prevSelection, [EditOperation.delete(deletionRange)], _edits => prevSelection); - - if (editor) { - editor.setSelections(prevSelection); - } - } -} - -class FormatOnSaveParticipant implements ISaveParticipantParticipant { - - constructor( - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - ) { - // Nothing - } - - async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, progress: IProgress, token: CancellationToken): Promise { - - const model = editorModel.textEditorModel; - const overrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: model.uri }; - - if (env.reason === SaveReason.AUTO || !this._configurationService.getValue('editor.formatOnSave', overrides)) { - return undefined; - } - - progress.report({ message: localize('formatting', "Formatting") }); - const editorOrModel = findEditor(model, this._codeEditorService) || model; - await this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, token); - } -} - -class CodeActionOnSaveParticipant implements ISaveParticipantParticipant { - - constructor( - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - ) { } - - async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, progress: IProgress, token: CancellationToken): Promise { - if (env.reason === SaveReason.AUTO) { - return undefined; - } - const model = editorModel.textEditorModel; - - const settingsOverrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: editorModel.resource }; - const setting = this._configurationService.getValue<{ [kind: string]: boolean }>('editor.codeActionsOnSave', settingsOverrides); - if (!setting) { - return undefined; - } - - const codeActionsOnSave = Object.keys(setting) - .filter(x => setting[x]).map(x => new CodeActionKind(x)) - .sort((a, b) => { - if (CodeActionKind.SourceFixAll.contains(a)) { - if (CodeActionKind.SourceFixAll.contains(b)) { - return 0; - } - return -1; - } - if (CodeActionKind.SourceFixAll.contains(b)) { - return 1; - } - return 0; - }); - - if (!codeActionsOnSave.length) { - return undefined; - } - - const excludedActions = Object.keys(setting) - .filter(x => setting[x] === false) - .map(x => new CodeActionKind(x)); - - progress.report({ message: localize('codeaction', "Quick Fixes") }); - await this.applyOnSaveActions(model, codeActionsOnSave, excludedActions, token); - } - - private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly CodeActionKind[], excludes: readonly CodeActionKind[], token: CancellationToken): Promise { - for (const codeActionKind of codeActionsOnSave) { - const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, token); - try { - await this.applyCodeActions(actionsToRun.validActions); - } catch { - // Failure to apply a code action should not block other on save actions - } finally { - actionsToRun.dispose(); - } - } - } - - private async applyCodeActions(actionsToRun: readonly CodeAction[]) { - for (const action of actionsToRun) { - await this._instantiationService.invokeFunction(applyCodeAction, action); - } - } - - private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, excludes: readonly CodeActionKind[], token: CancellationToken) { - return getCodeActions(model, model.getFullModelRange(), { - type: CodeActionTriggerType.Auto, - filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true }, - }, token); - } -} - -class ExtHostSaveParticipant implements ISaveParticipantParticipant { +class ExtHostSaveParticipant implements ITextFileSaveParticipant { private readonly _proxy: ExtHostDocumentSaveParticipantShape; @@ -336,65 +51,19 @@ class ExtHostSaveParticipant implements ISaveParticipantParticipant { // The save participant can change a model before its saved to support various scenarios like trimming trailing whitespace @extHostCustomer -export class SaveParticipant implements ISaveParticipant { +export class SaveParticipant { - private readonly _saveParticipants: IdleValue; + private _saveParticipantDisposable: IDisposable; constructor( extHostContext: IExtHostContext, @IInstantiationService instantiationService: IInstantiationService, - @IProgressService private readonly _progressService: IProgressService, - @ILogService private readonly _logService: ILogService, - @ILabelService private readonly _labelService: ILabelService, @ITextFileService private readonly _textFileService: ITextFileService ) { - this._saveParticipants = new IdleValue(() => [ - instantiationService.createInstance(TrimWhitespaceParticipant), - instantiationService.createInstance(CodeActionOnSaveParticipant), - instantiationService.createInstance(FormatOnSaveParticipant), - instantiationService.createInstance(FinalNewLineParticipant), - instantiationService.createInstance(TrimFinalNewLinesParticipant), - instantiationService.createInstance(ExtHostSaveParticipant, extHostContext), - ]); - // Set as save participant for all text files - this._textFileService.saveParticipant = this; + this._saveParticipantDisposable = this._textFileService.files.addSaveParticipant(instantiationService.createInstance(ExtHostSaveParticipant, extHostContext)); } dispose(): void { - this._textFileService.saveParticipant = undefined; - this._saveParticipants.dispose(); - } - - async participate(model: IResolvedTextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise { - - const cts = new CancellationTokenSource(token); - - return this._progressService.withProgress({ - title: localize('saveParticipants', "Running Save Participants for '{0}'", this._labelService.getUriLabel(model.resource, { relative: true })), - location: ProgressLocation.Notification, - cancellable: true, - delay: model.isDirty() ? 3000 : 5000 - }, async progress => { - // undoStop before participation - model.textEditorModel.pushStackElement(); - - for (let p of this._saveParticipants.getValue()) { - if (cts.token.isCancellationRequested) { - break; - } - try { - const promise = p.participate(model, context, progress, cts.token); - await raceCancellation(promise, cts.token); - } catch (err) { - this._logService.warn(err); - } - } - - // undoStop after participation - model.textEditorModel.pushStackElement(); - }, () => { - // user cancel - cts.dispose(true); - }); + this._saveParticipantDisposable.dispose(); } } diff --git a/src/vs/workbench/api/browser/mainThreadTimeline.ts b/src/vs/workbench/api/browser/mainThreadTimeline.ts index 919e4a18ff5b..428bf0ed2d95 100644 --- a/src/vs/workbench/api/browser/mainThreadTimeline.ts +++ b/src/vs/workbench/api/browser/mainThreadTimeline.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { MainContext, MainThreadTimelineShape, IExtHostContext, ExtHostTimelineShape, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { ITimelineService, TimelineItem, TimelineProviderDescriptor, TimelineChangeEvent } from 'vs/workbench/contrib/timeline/common/timeline'; +import { TimelineChangeEvent, TimelineCursor, TimelineProviderDescriptor, ITimelineService } from 'vs/workbench/contrib/timeline/common/timeline'; @extHostNamedCustomer(MainContext.MainThreadTimeline) export class MainThreadTimeline implements MainThreadTimelineShape { @@ -24,10 +24,6 @@ export class MainThreadTimeline implements MainThreadTimelineShape { this._proxy = context.getProxy(ExtHostContext.ExtHostTimeline); } - $getTimeline(uri: URI, token: CancellationToken): Promise { - return this._timelineService.getTimeline(uri, token); - } - $registerTimelineProvider(provider: TimelineProviderDescriptor): void { this.logService.trace(`MainThreadTimeline#registerTimelineProvider: id=${provider.id}`); @@ -43,8 +39,8 @@ export class MainThreadTimeline implements MainThreadTimelineShape { this._timelineService.registerTimelineProvider({ ...provider, onDidChange: onDidChange.event, - provideTimeline(uri: URI, token: CancellationToken) { - return proxy.$getTimeline(provider.id, uri, token); + provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }) { + return proxy.$getTimeline(provider.id, uri, cursor, token, options); }, dispose() { emitters.delete(provider.id); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 3952717d2206..0d25c1176e75 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -133,7 +133,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHostLabelService, new ExtHostLabelService(rpcProtocol)); const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); - const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol)); + const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); // Check that no named customers are missing const expected: ProxyIdentifier[] = values(ExtHostContext); @@ -345,6 +345,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerHoverProvider(selector: vscode.DocumentSelector, provider: vscode.HoverProvider): vscode.Disposable { return extHostLanguageFeatures.registerHoverProvider(extension, checkSelector(selector), provider, extension.identifier); }, + registerEvaluatableExpressionProvider(selector: vscode.DocumentSelector, provider: vscode.EvaluatableExpressionProvider): vscode.Disposable { + return extHostLanguageFeatures.registerEvaluatableExpressionProvider(extension, checkSelector(selector), provider, extension.identifier); + }, registerDocumentHighlightProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentHighlightProvider): vscode.Disposable { return extHostLanguageFeatures.registerDocumentHighlightProvider(extension, checkSelector(selector), provider); }, @@ -926,6 +929,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I DocumentLink: extHostTypes.DocumentLink, DocumentSymbol: extHostTypes.DocumentSymbol, EndOfLine: extHostTypes.EndOfLine, + EvaluatableExpression: extHostTypes.EvaluatableExpression, EventEmitter: Emitter, ExtensionKind: extHostTypes.ExtensionKind, CustomExecution: extHostTypes.CustomExecution, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c181215c41d3..b780bccbd4d4 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -49,7 +49,7 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; -import { TimelineItem, TimelineProviderDescriptor, TimelineChangeEvent, TimelineItemWithSource } from 'vs/workbench/contrib/timeline/common/timeline'; +import { Timeline, TimelineChangeEvent, TimelineCursor, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -353,6 +353,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerImplementationSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerTypeDefinitionSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerHoverProvider(handle: number, selector: IDocumentFilterDto[]): void; + $registerEvaluatableExpressionProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerDocumentHighlightProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto): void; @@ -802,8 +803,6 @@ export interface MainThreadTimelineShape extends IDisposable { $registerTimelineProvider(provider: TimelineProviderDescriptor): void; $unregisterTimelineProvider(source: string): void; $emitTimelineChangeEvent(e: TimelineChangeEvent): void; - - $getTimeline(uri: UriComponents, token: CancellationToken): Promise; } // -- extension host @@ -1208,6 +1207,7 @@ export interface ExtHostLanguageFeaturesShape { $provideImplementation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideTypeDefinition(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideHover(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; + $provideEvaluatableExpression(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideReferences(handle: number, resource: UriComponents, position: IPosition, context: modes.ReferenceContext, token: CancellationToken): Promise; $provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise; @@ -1457,7 +1457,7 @@ export interface ExtHostTunnelServiceShape { } export interface ExtHostTimelineShape { - $getTimeline(source: string, uri: UriComponents, token: CancellationToken): Promise; + $getTimeline(source: string, uri: UriComponents, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }): Promise; } // --- proxy identifiers diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index b90e3ddeb24f..65d12628b105 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -276,6 +276,27 @@ class HoverAdapter { } } +class EvaluatableExpressionAdapter { + + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.EvaluatableExpressionProvider, + ) { } + + public provideEvaluatableExpression(resource: URI, position: IPosition, token: CancellationToken): Promise { + + const doc = this._documents.getDocument(resource); + const pos = typeConvert.Position.to(position); + + return asPromise(() => this._provider.provideEvaluatableExpression(doc, pos, token)).then(value => { + if (value) { + return typeConvert.EvaluatableExpression.from(value); + } + return undefined; + }); + } +} + class DocumentHighlightAdapter { constructor( @@ -1329,7 +1350,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter - | SelectionRangeAdapter | CallHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter; + | SelectionRangeAdapter | CallHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter; class AdapterData { constructor( @@ -1549,6 +1570,18 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, HoverAdapter, adapter => adapter.provideHover(URI.revive(resource), position, token), undefined); } + // --- debug hover + + registerEvaluatableExpressionProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.EvaluatableExpressionProvider, extensionId?: ExtensionIdentifier): vscode.Disposable { + const handle = this._addNewAdapter(new EvaluatableExpressionAdapter(this._documents, provider), extension); + this._proxy.$registerEvaluatableExpressionProvider(handle, this._transformDocumentSelector(selector)); + return this._createDisposable(handle); + } + + $provideEvaluatableExpression(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise { + return this._withAdapter(handle, EvaluatableExpressionAdapter, adapter => adapter.provideEvaluatableExpression(URI.revive(resource), position, token), undefined); + } + // --- occurrences registerDocumentHighlightProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentHighlightProvider): vscode.Disposable { diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts index 1ce8f7813ffd..f5f63a2ce4dc 100644 --- a/src/vs/workbench/api/common/extHostTimeline.ts +++ b/src/vs/workbench/api/common/extHostTimeline.ts @@ -7,61 +7,91 @@ import * as vscode from 'vscode'; import { UriComponents, URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ExtHostTimelineShape, MainThreadTimelineShape, IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { TimelineItemWithSource, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; +import { Timeline, TimelineCursor, TimelineItem, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; import { IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; +import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ThemeIcon } from 'vs/workbench/api/common/extHostTypes'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export interface IExtHostTimeline extends ExtHostTimelineShape { readonly _serviceBrand: undefined; - $getTimeline(id: string, uri: UriComponents, token: vscode.CancellationToken): Promise; + $getTimeline(id: string, uri: UriComponents, cursor: vscode.TimelineCursor, token: vscode.CancellationToken, options?: { cacheResults?: boolean }): Promise; } export const IExtHostTimeline = createDecorator('IExtHostTimeline'); export class ExtHostTimeline implements IExtHostTimeline { + private static handlePool = 0; + _serviceBrand: undefined; private _proxy: MainThreadTimelineShape; private _providers = new Map(); + private _itemsBySourceByUriMap = new Map>>(); + constructor( mainContext: IMainContext, + commands: ExtHostCommands, ) { this._proxy = mainContext.getProxy(MainContext.MainThreadTimeline); + + commands.registerArgumentProcessor({ + processArgument: arg => { + if (arg && arg.$mid === 11) { + const uri = arg.uri === undefined ? undefined : URI.revive(arg.uri); + return this._itemsBySourceByUriMap.get(getUriKey(uri))?.get(arg.source)?.get(arg.handle); + } + + return arg; + } + }); } - async $getTimeline(id: string, uri: UriComponents, token: vscode.CancellationToken): Promise { + async $getTimeline(id: string, uri: UriComponents, cursor: vscode.TimelineCursor, token: vscode.CancellationToken, options?: { cacheResults?: boolean }): Promise { const provider = this._providers.get(id); - return provider?.provideTimeline(URI.revive(uri), token) ?? []; + return provider?.provideTimeline(URI.revive(uri), cursor, token, options); } - registerTimelineProvider(scheme: string | string[], provider: vscode.TimelineProvider, extensionId: ExtensionIdentifier, commandConverter: CommandsConverter): IDisposable { + registerTimelineProvider(scheme: string | string[], provider: vscode.TimelineProvider, _extensionId: ExtensionIdentifier, commandConverter: CommandsConverter): IDisposable { const timelineDisposables = new DisposableStore(); - const convertTimelineItem = this.convertTimelineItem(provider.id, commandConverter, timelineDisposables); + const convertTimelineItem = this.convertTimelineItem(provider.id, commandConverter, timelineDisposables).bind(this); let disposable: IDisposable | undefined; if (provider.onDidChange) { disposable = provider.onDidChange(this.emitTimelineChangeEvent(provider.id), this); } + const itemsBySourceByUriMap = this._itemsBySourceByUriMap; return this.registerTimelineProviderCore({ ...provider, scheme: scheme, onDidChange: undefined, - async provideTimeline(uri: URI, token: CancellationToken) { + async provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }) { timelineDisposables.clear(); - const results = await provider.provideTimeline(uri, token); + // For now, only allow the caching of a single Uri + if (options?.cacheResults && !itemsBySourceByUriMap.has(getUriKey(uri))) { + itemsBySourceByUriMap.clear(); + } + + const result = await provider.provideTimeline(uri, cursor, token); // Intentional == we don't know how a provider will respond // eslint-disable-next-line eqeqeq - return results != null - ? results.map(item => convertTimelineItem(item)) - : []; + if (result == null) { + return undefined; + } + + // TODO: Determine if we should cache dependent on who calls us (internal vs external) + const convertItem = convertTimelineItem(uri, options?.cacheResults ?? false); + return { + ...result, + source: provider.id, + items: result.items.map(convertItem) + }; }, dispose() { disposable?.dispose(); @@ -70,39 +100,72 @@ export class ExtHostTimeline implements IExtHostTimeline { }); } - private convertTimelineItem(source: string, commandConverter: CommandsConverter, disposables: DisposableStore): (item: vscode.TimelineItem) => TimelineItemWithSource { - return (item: vscode.TimelineItem) => { - const { iconPath, ...props } = item; + private convertTimelineItem(source: string, commandConverter: CommandsConverter, disposables: DisposableStore) { + return (uri: URI, cacheResults: boolean) => { + let itemsMap: Map | undefined; + if (cacheResults) { + const uriKey = getUriKey(uri); - let icon; - let iconDark; - let themeIcon; - if (item.iconPath) { - if (iconPath instanceof ThemeIcon) { - themeIcon = { id: iconPath.id }; + let sourceMap = this._itemsBySourceByUriMap.get(uriKey); + if (sourceMap === undefined) { + sourceMap = new Map(); + this._itemsBySourceByUriMap.set(uriKey, sourceMap); } - else if (URI.isUri(iconPath)) { - icon = iconPath; - iconDark = iconPath; - } - else { - ({ light: icon, dark: iconDark } = iconPath as { light: URI; dark: URI }); + + itemsMap = sourceMap.get(source); + if (itemsMap === undefined) { + itemsMap = new Map(); + sourceMap.set(source, itemsMap); } } - return { - ...props, - source: source, - command: item.command ? commandConverter.toInternal(item.command, disposables) : undefined, - icon: icon, - iconDark: iconDark, - themeIcon: themeIcon + return (item: vscode.TimelineItem): TimelineItem => { + const { iconPath, ...props } = item; + + const handle = `${source}|${item.id ?? `${item.timestamp}-${ExtHostTimeline.handlePool++}`}`; + itemsMap?.set(handle, item); + + let icon; + let iconDark; + let themeIcon; + if (item.iconPath) { + if (iconPath instanceof ThemeIcon) { + themeIcon = { id: iconPath.id }; + } + else if (URI.isUri(iconPath)) { + icon = iconPath; + iconDark = iconPath; + } + else { + ({ light: icon, dark: iconDark } = iconPath as { light: URI; dark: URI }); + } + } + + return { + ...props, + handle: handle, + source: source, + command: item.command ? commandConverter.toInternal(item.command, disposables) : undefined, + icon: icon, + iconDark: iconDark, + themeIcon: themeIcon + }; }; }; } private emitTimelineChangeEvent(id: string) { return (e: vscode.TimelineChangeEvent) => { + // Clear caches + if (e?.uri === undefined) { + for (const sourceMap of this._itemsBySourceByUriMap.values()) { + sourceMap.get(id)?.clear(); + } + } + else { + this._itemsBySourceByUriMap.get(getUriKey(e.uri))?.clear(); + } + this._proxy.$emitTimelineChangeEvent({ ...e, id: id }); }; } @@ -123,9 +186,18 @@ export class ExtHostTimeline implements IExtHostTimeline { this._providers.set(provider.id, provider); return toDisposable(() => { + for (const sourceMap of this._itemsBySourceByUriMap.values()) { + sourceMap.get(provider.id)?.clear(); + } + this._providers.delete(provider.id); this._proxy.$unregisterTimelineProvider(provider.id); provider.dispose(); }); } } + +function getUriKey(uri: URI | undefined): string | undefined { + return uri?.toString(); +} + diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 72c238ee6ba7..6027377e166d 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -257,7 +257,7 @@ export namespace MarkdownString { } else if (htmlContent.isMarkdownString(markup)) { res = markup; } else if (typeof markup === 'string') { - res = { value: markup }; + res = { value: markup }; } else { res = { value: '' }; } @@ -737,6 +737,20 @@ export namespace Hover { return new types.Hover(info.contents.map(MarkdownString.to), Range.to(info.range)); } } + +export namespace EvaluatableExpression { + export function from(expression: vscode.EvaluatableExpression): modes.EvaluatableExpression { + return { + range: Range.from(expression.range), + expression: expression.expression + }; + } + + export function to(info: modes.EvaluatableExpression): types.EvaluatableExpression { + return new types.EvaluatableExpression(Range.to(info.range), info.expression); + } +} + export namespace DocumentHighlight { export function from(documentHighlight: vscode.DocumentHighlight): modes.DocumentHighlight { return { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index f9f9de267584..f6871d7e2bda 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2283,6 +2283,17 @@ export class DebugAdapterInlineImplementation implements vscode.DebugAdapterInli } } +@es5ClassCompat +export class EvaluatableExpression implements vscode.EvaluatableExpression { + readonly range: vscode.Range; + readonly expression?: string; + + constructor(range: vscode.Range, expression?: string) { + this.range = range; + this.expression = expression; + } +} + export enum LogLevel { Trace = 1, Debug = 2, diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index c79a3c16ab05..ab474c9825f7 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -52,6 +52,8 @@ namespace schema { case 'comments/comment/title': return MenuId.CommentTitle; case 'comments/comment/context': return MenuId.CommentActions; case 'extension/context': return MenuId.ExtensionContext; + case 'timeline/title': return MenuId.TimelineTitle; + case 'timeline/item/context': return MenuId.TimelineItemContext; } return undefined; @@ -215,6 +217,16 @@ namespace schema { type: 'array', items: menuItem }, + 'timeline/title': { + description: localize('view.timelineTitle', "The Timeline view title menu"), + type: 'array', + items: menuItem + }, + 'timeline/item/context': { + description: localize('view.timelineContext', "The Timeline view item context menu"), + type: 'array', + items: menuItem + }, } }; diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 5c87849e9ea8..bcbae8f4e2dd 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -239,7 +239,7 @@ enum Redraw { class ResourceLabelWidget extends IconLabel { private _onDidRender = this._register(new Emitter()); - readonly onDidRender: Event = this._onDidRender.event; + readonly onDidRender = this._onDidRender.event; private readonly renderDisposables = this._register(new DisposableStore()); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index f94fe18534fc..66320713617f 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { EventType, addDisposableListener, addClass, removeClass, isAncestor, getClientArea, Dimension, toggleClass, position, size } from 'vs/base/browser/dom'; import { onDidChangeFullscreen, isFullscreen } from 'vs/base/browser/browser'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; @@ -87,26 +87,26 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi //#region Events - private readonly _onZenModeChange: Emitter = this._register(new Emitter()); - readonly onZenModeChange: Event = this._onZenModeChange.event; + private readonly _onZenModeChange = this._register(new Emitter()); + readonly onZenModeChange = this._onZenModeChange.event; - private readonly _onFullscreenChange: Emitter = this._register(new Emitter()); - readonly onFullscreenChange: Event = this._onFullscreenChange.event; + private readonly _onFullscreenChange = this._register(new Emitter()); + readonly onFullscreenChange = this._onFullscreenChange.event; - private readonly _onCenteredLayoutChange: Emitter = this._register(new Emitter()); - readonly onCenteredLayoutChange: Event = this._onCenteredLayoutChange.event; + private readonly _onCenteredLayoutChange = this._register(new Emitter()); + readonly onCenteredLayoutChange = this._onCenteredLayoutChange.event; - private readonly _onMaximizeChange: Emitter = this._register(new Emitter()); - readonly onMaximizeChange: Event = this._onMaximizeChange.event; + private readonly _onMaximizeChange = this._register(new Emitter()); + readonly onMaximizeChange = this._onMaximizeChange.event; - private readonly _onPanelPositionChange: Emitter = this._register(new Emitter()); - readonly onPanelPositionChange: Event = this._onPanelPositionChange.event; + private readonly _onPanelPositionChange = this._register(new Emitter()); + readonly onPanelPositionChange = this._onPanelPositionChange.event; - private readonly _onPartVisibilityChange: Emitter = this._register(new Emitter()); - readonly onPartVisibilityChange: Event = this._onPartVisibilityChange.event; + private readonly _onPartVisibilityChange = this._register(new Emitter()); + readonly onPartVisibilityChange = this._onPartVisibilityChange.event; private readonly _onLayout = this._register(new Emitter()); - readonly onLayout: Event = this._onLayout.event; + readonly onLayout = this._onLayout.event; //#endregion diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index 1e07ec234517..cec01038567c 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -34,7 +34,7 @@ export abstract class Part extends Component implements ISerializableView { get dimension(): Dimension | undefined { return this._dimension; } protected _onDidVisibilityChange = this._register(new Emitter()); - readonly onDidVisibilityChange: Event = this._onDidVisibilityChange.event; + readonly onDidVisibilityChange = this._onDidVisibilityChange.event; private parent: HTMLElement | undefined; private titleArea: HTMLElement | undefined; diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 4f824d94c74a..4ed709af0805 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -19,7 +19,7 @@ import { Widget } from 'vs/base/browser/ui/widget'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; import { ITheme } from 'vs/platform/theme/common/themeService'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; export interface ICompositeBarItem { id: string; @@ -61,7 +61,7 @@ export class CompositeBar extends Widget implements ICompositeBar { private compositeTransfer: LocalSelectionTransfer; private readonly _onDidChange: Emitter = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; + readonly onDidChange = this._onDidChange.event; constructor( items: ICompositeBarItem[], diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index 583bdb727a74..19781fbb59f9 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -39,7 +39,7 @@ export abstract class BaseEditor extends Panel implements IEditor { readonly minimumHeight = DEFAULT_EDITOR_MIN_DIMENSIONS.height; readonly maximumHeight = DEFAULT_EDITOR_MAX_DIMENSIONS.height; - readonly onDidSizeConstraintsChange: Event<{ width: number; height: number; } | undefined> = Event.None; + readonly onDidSizeConstraintsChange = Event.None; protected _input: EditorInput | undefined; protected _options: EditorOptions | undefined; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 11f5d5a0a04e..cb05c88239b5 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -1580,7 +1580,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { get maximumHeight(): number { return this.editorControl.maximumHeight; } private _onDidChange = this._register(new Relay<{ width: number; height: number; } | undefined>()); - readonly onDidChange: Event<{ width: number; height: number; } | undefined> = this._onDidChange.event; + readonly onDidChange = this._onDidChange.event; layout(width: number, height: number): void { this.dimension = new Dimension(width, height); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts b/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts index 94a5a660b43d..fae7f87f7cbd 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts @@ -23,10 +23,10 @@ export class NotificationsAlerts extends Disposable { } private registerListeners(): void { - this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); + this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e))); } - private onDidNotificationChange(e: INotificationChangeEvent): void { + private onDidChangeNotification(e: INotificationChangeEvent): void { if (e.kind === NotificationChangeType.ADD) { // ARIA alert for screen readers @@ -46,7 +46,7 @@ export class NotificationsAlerts extends Disposable { private triggerAriaAlert(notifiation: INotificationViewItem): void { // Trigger the alert again whenever the label changes - const listener = notifiation.onDidLabelChange(e => { + const listener = notifiation.onDidChangeLabel(e => { if (e.kind === NotificationViewItemLabelKind.MESSAGE) { this.doTriggerAriaAlert(notifiation); } @@ -69,4 +69,4 @@ export class NotificationsAlerts extends Disposable { alert(alertText); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 6d27e856dbba..0b4cbe772643 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -58,7 +58,7 @@ export class NotificationsCenter extends Themable { } private registerListeners(): void { - this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); + this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e))); this._register(this.layoutService.onLayout(dimension => this.layout(dimension))); } @@ -167,7 +167,7 @@ export class NotificationsCenter extends Themable { return keybinding ? keybinding.getLabel() : null; } - private onDidNotificationChange(e: INotificationChangeEvent): void { + private onDidChangeNotification(e: INotificationChangeEvent): void { if (!this._isVisible) { return; // only if visible } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index ae4d2e69aff3..2d585698cb9b 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsList.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsList.ts @@ -181,7 +181,7 @@ export class NotificationsList extends Themable { } // Restore DOM focus if we had focus before - if (listHasDOMFocus) { + if (this.isVisible && listHasDOMFocus) { list.domFocus(); } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index 97c9af2a6221..3545ab9c02b6 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -34,11 +34,11 @@ export class NotificationsStatus extends Disposable { } private registerListeners(): void { - this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); - this._register(this.model.onDidStatusMessageChange(e => this.onDidStatusMessageChange(e))); + this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e))); + this._register(this.model.onDidChangeStatusMessage(e => this.onDidChangeStatusMessage(e))); } - private onDidNotificationChange(e: INotificationChangeEvent): void { + private onDidChangeNotification(e: INotificationChangeEvent): void { if (this.isNotificationsCenterVisible) { return; // no change if notification center is visible } @@ -101,7 +101,7 @@ export class NotificationsStatus extends Disposable { } } - private onDidStatusMessageChange(e: IStatusMessageChangeEvent): void { + private onDidChangeStatusMessage(e: IStatusMessageChangeEvent): void { const statusItem = e.item; switch (e.kind) { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index ee6bbffc2833..9461b4887ac7 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -90,11 +90,11 @@ export class NotificationsToasts extends Themable { this.model.notifications.forEach(notification => this.addToast(notification)); // Update toasts on notification changes - this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); + this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e))); }); // Filter - this._register(this.model.onDidFilterChange(filter => { + this._register(this.model.onDidChangeFilter(filter => { if (filter === NotificationsFilter.SILENT || filter === NotificationsFilter.ERROR) { this.hide(); } @@ -114,7 +114,7 @@ export class NotificationsToasts extends Themable { ]); } - private onDidNotificationChange(e: INotificationChangeEvent): void { + private onDidChangeNotification(e: INotificationChangeEvent): void { switch (e.kind) { case NotificationChangeType.ADD: return this.addToast(e.item); @@ -194,12 +194,12 @@ export class NotificationsToasts extends Themable { this.layoutContainer(maxDimensions.height); // Update when item height changes due to expansion - itemDisposables.add(item.onDidExpansionChange(() => { + itemDisposables.add(item.onDidChangeExpansion(() => { notificationList.updateNotificationsList(0, 1, [item]); })); // Update when item height potentially changes due to label changes - itemDisposables.add(item.onDidLabelChange(e => { + itemDisposables.add(item.onDidChangeLabel(e => { if (!item.expanded) { return; // dynamic height only applies to expanded notifications } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index a41db2f2ee11..318307a84774 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -345,7 +345,7 @@ export class NotificationTemplateRenderer extends Disposable { this.renderProgress(notification); // Label Change Events - this.inputDisposables.add(notification.onDidLabelChange(event => { + this.inputDisposables.add(notification.onDidChangeLabel(event => { switch (event.kind) { case NotificationViewItemLabelKind.SEVERITY: this.renderSeverity(notification); diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index b1cdfc6f4eb7..6ee4f8cff776 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -56,7 +56,7 @@ export class PanelPart extends CompositePart implements IPanelService { //#region IView - readonly minimumWidth: number = 420; + readonly minimumWidth: number = 300; readonly maximumWidth: number = Number.POSITIVE_INFINITY; readonly minimumHeight: number = 77; readonly maximumHeight: number = Number.POSITIVE_INFINITY; @@ -76,7 +76,7 @@ export class PanelPart extends CompositePart implements IPanelService { //#endregion get onDidPanelOpen(): Event<{ panel: IPanel, focus: boolean; }> { return Event.map(this.onDidCompositeOpen.event, compositeOpen => ({ panel: compositeOpen.composite, focus: compositeOpen.focus })); } - readonly onDidPanelClose: Event = this.onDidCompositeClose.event; + readonly onDidPanelClose = this.onDidCompositeClose.event; private activePanelContextKey: IContextKey; private panelFocusContextKey: IContextKey; diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 2e4de06c27b2..bd12bd60f9b9 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -71,7 +71,7 @@ display: none; } -.monaco-workbench .pane > .pane-body > .empty-view { +.monaco-workbench .pane > .pane-body > .welcome-view { width: 100%; height: 100%; padding: 0 20px 0 20px; @@ -79,12 +79,12 @@ box-sizing: border-box; } -.monaco-workbench .pane > .pane-body:not(.empty) > .empty-view, -.monaco-workbench .pane > .pane-body.empty > :not(.empty-view) { +.monaco-workbench .pane > .pane-body:not(.welcome) > .welcome-view, +.monaco-workbench .pane > .pane-body.welcome > :not(.welcome-view) { display: none; } -.monaco-workbench .pane > .pane-body > .empty-view .monaco-button { +.monaco-workbench .pane > .pane-body > .welcome-view .monaco-button { max-width: 260px; margin-left: auto; margin-right: auto; diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 7c37ed3075a8..074150b643d5 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -25,7 +25,7 @@ import { PaneView, IPaneViewOptions, IPaneOptions, Pane, DefaultPaneDndControlle import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } 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 } from 'vs/workbench/common/views'; +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'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { assertIsDefined } from 'vs/base/common/types'; @@ -38,7 +38,7 @@ import { Component } from 'vs/workbench/common/component'; import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; -import { parseLinkedText } from 'vs/base/browser/linkedText'; +import { parseLinkedText } from 'vs/base/common/linkedText'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Button } from 'vs/base/browser/ui/button/button'; import { Link } from 'vs/platform/opener/browser/link'; @@ -60,6 +60,88 @@ export interface IViewPaneOptions extends IPaneOptions { const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); +interface IItem { + readonly descriptor: IViewContentDescriptor; + visible: boolean; +} + +class ViewWelcomeController { + + private _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + private defaultItem: IItem | undefined; + private items: IItem[] = []; + get contents(): IViewContentDescriptor[] { + const visibleItems = this.items.filter(v => v.visible); + + if (visibleItems.length === 0 && this.defaultItem) { + return [this.defaultItem.descriptor]; + } + + return visibleItems.map(v => v.descriptor); + } + + private contextKeyService: IContextKeyService; + private disposables = new DisposableStore(); + + constructor( + private id: string, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + this.contextKeyService = contextKeyService.createScoped(); + this.disposables.add(this.contextKeyService); + + contextKeyService.onDidChangeContext(this.onDidChangeContext, this, this.disposables); + Event.filter(viewsRegistry.onDidChangeViewWelcomeContent, id => id === this.id)(this.onDidChangeViewWelcomeContent, this, this.disposables); + this.onDidChangeViewWelcomeContent(); + } + + private onDidChangeViewWelcomeContent(): void { + const descriptors = viewsRegistry.getViewWelcomeContent(this.id); + + this.items = []; + + for (const descriptor of descriptors) { + if (descriptor.when === 'default') { + this.defaultItem = { descriptor, visible: true }; + } else { + const visible = descriptor.when ? this.contextKeyService.contextMatchesRules(descriptor.when) : true; + this.items.push({ descriptor, visible }); + } + } + + this._onDidChange.fire(); + } + + private onDidChangeContext(): void { + let didChange = false; + + for (const item of this.items) { + if (!item.descriptor.when || item.descriptor.when === 'default') { + continue; + } + + const visible = this.contextKeyService.contextMatchesRules(item.descriptor.when); + + if (item.visible === visible) { + continue; + } + + item.visible = visible; + didChange = true; + } + + if (didChange) { + this._onDidChange.fire(); + } + } + + dispose(): void { + this.disposables.dispose(); + } +} + export abstract class ViewPane extends Pane implements IView { private static readonly AlwaysShowActionsConfig = 'workbench.view.alwaysShowHeaderActions'; @@ -76,8 +158,8 @@ export abstract class ViewPane extends Pane implements IView { protected _onDidChangeTitleArea = this._register(new Emitter()); readonly onDidChangeTitleArea: Event = this._onDidChangeTitleArea.event; - protected _onDidChangeEmptyState = this._register(new Emitter()); - readonly onDidChangeEmptyState: Event = this._onDidChangeEmptyState.event; + protected _onDidChangeViewWelcomeState = this._register(new Emitter()); + readonly onDidChangeViewWelcomeState: Event = this._onDidChangeViewWelcomeState.event; private focusedViewContextKey: IContextKey; @@ -95,8 +177,9 @@ export abstract class ViewPane extends Pane implements IView { protected twistiesContainer?: HTMLElement; private bodyContainer!: HTMLElement; - private emptyViewContainer!: HTMLElement; - private emptyViewDisposable: IDisposable = Disposable.None; + private viewWelcomeContainer!: HTMLElement; + private viewWelcomeDisposable: IDisposable = Disposable.None; + private viewWelcomeController: ViewWelcomeController; constructor( options: IViewPaneOptions, @@ -119,6 +202,8 @@ export abstract class ViewPane extends Pane implements IView { this.menuActions = this._register(instantiationService.createInstance(ViewMenuActions, this.id, options.titleMenuId || MenuId.ViewTitle, MenuId.ViewTitleContext)); this._register(this.menuActions.onDidChangeTitle(() => this.updateActions())); + + this.viewWelcomeController = new ViewWelcomeController(this.id, contextKeyService); } setVisible(visible: boolean): void { @@ -206,18 +291,15 @@ export abstract class ViewPane extends Pane implements IView { protected renderBody(container: HTMLElement): void { this.bodyContainer = container; - this.emptyViewContainer = append(container, $('.empty-view', { tabIndex: 0 })); + this.viewWelcomeContainer = append(container, $('.welcome-view', { tabIndex: 0 })); - // we should update our empty state whenever - const onEmptyViewContentChange = Event.any( - // the registry changes - Event.map(Event.filter(viewsRegistry.onDidChangeEmptyViewContent, id => id === this.id), () => this.isEmpty()), - // or the view's empty state changes - Event.latch(Event.map(this.onDidChangeEmptyState, () => this.isEmpty())) - ); + const onViewWelcomeChange = Event.any(this.viewWelcomeController.onDidChange, this.onDidChangeViewWelcomeState); + this._register(onViewWelcomeChange(this.updateViewWelcome, this)); + this.updateViewWelcome(); + } - this._register(onEmptyViewContentChange(this.updateEmptyState, this)); - this.updateEmptyState(this.isEmpty()); + protected layoutBody(height: number, width: number): void { + // noop } protected getProgressLocation(): string { @@ -286,26 +368,26 @@ export abstract class ViewPane extends Pane implements IView { // Subclasses to implement for saving state } - private updateEmptyState(isEmpty: boolean): void { - this.emptyViewDisposable.dispose(); + private updateViewWelcome(): void { + this.viewWelcomeDisposable.dispose(); - if (!isEmpty) { - removeClass(this.bodyContainer, 'empty'); - this.emptyViewContainer.innerHTML = ''; + if (!this.shouldShowWelcome()) { + removeClass(this.bodyContainer, 'welcome'); + this.viewWelcomeContainer.innerHTML = ''; return; } - const contents = viewsRegistry.getEmptyViewContent(this.id); + const contents = this.viewWelcomeController.contents; if (contents.length === 0) { - removeClass(this.bodyContainer, 'empty'); - this.emptyViewContainer.innerHTML = ''; + removeClass(this.bodyContainer, 'welcome'); + this.viewWelcomeContainer.innerHTML = ''; return; } const disposables = new DisposableStore(); - addClass(this.bodyContainer, 'empty'); - this.emptyViewContainer.innerHTML = ''; + addClass(this.bodyContainer, 'welcome'); + this.viewWelcomeContainer.innerHTML = ''; for (const { content } of contents) { const lines = content.split('\n'); @@ -317,13 +399,13 @@ export abstract class ViewPane extends Pane implements IView { continue; } - const p = append(this.emptyViewContainer, $('p')); + const p = append(this.viewWelcomeContainer, $('p')); const linkedText = parseLinkedText(line); - for (const node of linkedText) { + for (const node of linkedText.nodes) { if (typeof node === 'string') { append(p, document.createTextNode(node)); - } else if (linkedText.length === 1) { + } else if (linkedText.nodes.length === 1) { const button = new Button(p, { title: node.title }); button.label = node.label; button.onDidClick(_ => this.openerService.open(node.href), null, disposables); @@ -339,10 +421,10 @@ export abstract class ViewPane extends Pane implements IView { } } - this.emptyViewDisposable = disposables; + this.viewWelcomeDisposable = disposables; } - isEmpty(): boolean { + shouldShowWelcome(): boolean { return false; } } diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index 2ea6bcae00fe..ddcb76a0a955 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -68,7 +68,7 @@ export abstract class Viewlet extends PaneComposite implements IViewlet { */ export class ViewletDescriptor extends CompositeDescriptor { - public static create( + static create( ctor: { new(...services: Services): Viewlet }, id: string, name: string, diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 604e4d1e9142..a24435cd2417 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -6,7 +6,7 @@ import 'vs/workbench/browser/style'; import { localize } from 'vs/nls'; -import { Event, Emitter, setGlobalLeakWarningThreshold } from 'vs/base/common/event'; +import { Emitter, setGlobalLeakWarningThreshold } from 'vs/base/common/event'; import { addClasses, addClass, removeClasses } from 'vs/base/browser/dom'; import { runWhenIdle } from 'vs/base/common/async'; import { getZoomLevel, isFirefox, isSafari, isChrome } from 'vs/base/browser/browser'; @@ -50,13 +50,13 @@ import { Extensions as PanelExtensions, PanelRegistry } from 'vs/workbench/brows export class Workbench extends Layout { private readonly _onBeforeShutdown = this._register(new Emitter()); - readonly onBeforeShutdown: Event = this._onBeforeShutdown.event; + readonly onBeforeShutdown = this._onBeforeShutdown.event; private readonly _onWillShutdown = this._register(new Emitter()); - readonly onWillShutdown: Event = this._onWillShutdown.event; + readonly onWillShutdown = this._onWillShutdown.event; private readonly _onShutdown = this._register(new Emitter()); - readonly onShutdown: Event = this._onShutdown.event; + readonly onShutdown = this._onShutdown.event; constructor( parent: HTMLElement, diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index a6b19e5df2c8..ff932b00bd82 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -16,28 +16,29 @@ import { find, equals } from 'vs/base/common/arrays'; export interface INotificationsModel { - // - // Notifications as Toasts/Center - // + //#region Notifications as Toasts/Center readonly notifications: INotificationViewItem[]; - readonly onDidNotificationChange: Event; - readonly onDidFilterChange: Event; + readonly onDidChangeNotification: Event; + readonly onDidChangeFilter: Event; addNotification(notification: INotification): INotificationHandle; setFilter(filter: NotificationsFilter): void; - // - // Notifications as Status - // + //#endregion + + + //#region Notifications as Status readonly statusMessage: IStatusMessageViewItem | undefined; - readonly onDidStatusMessageChange: Event; + readonly onDidChangeStatusMessage: Event; showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable; + + //#endregion } export const enum NotificationChangeType { @@ -87,19 +88,22 @@ export interface IStatusMessageChangeEvent { kind: StatusMessageChangeType; } -export class NotificationHandle implements INotificationHandle { +export class NotificationHandle extends Disposable implements INotificationHandle { - private readonly _onDidClose: Emitter = new Emitter(); - readonly onDidClose: Event = this._onDidClose.event; + private readonly _onDidClose = this._register(new Emitter()); + readonly onDidClose = this._onDidClose.event; + + constructor(private readonly item: INotificationViewItem, private readonly onClose: (item: INotificationViewItem) => void) { + super(); - constructor(private readonly item: INotificationViewItem, private readonly closeItem: (item: INotificationViewItem) => void) { this.registerListeners(); } private registerListeners(): void { Event.once(this.item.onDidClose)(() => { this._onDidClose.fire(); - this._onDidClose.dispose(); + + this.dispose(); }); } @@ -120,8 +124,9 @@ export class NotificationHandle implements INotificationHandle { } close(): void { - this.closeItem(this.item); - this._onDidClose.dispose(); + this.onClose(this.item); + + this.dispose(); } } @@ -129,14 +134,14 @@ export class NotificationsModel extends Disposable implements INotificationsMode private static readonly NO_OP_NOTIFICATION = new NoOpNotification(); - private readonly _onDidNotificationChange = this._register(new Emitter()); - readonly onDidNotificationChange: Event = this._onDidNotificationChange.event; + private readonly _onDidChangeNotification = this._register(new Emitter()); + readonly onDidChangeNotification = this._onDidChangeNotification.event; - private readonly _onDidStatusMessageChange = this._register(new Emitter()); - readonly onDidStatusMessageChange: Event = this._onDidStatusMessageChange.event; + private readonly _onDidChangeStatusMessage = this._register(new Emitter()); + readonly onDidChangeStatusMessage = this._onDidChangeStatusMessage.event; - private readonly _onDidFilterChange = this._register(new Emitter()); - readonly onDidFilterChange: Event = this._onDidFilterChange.event; + private readonly _onDidChangeFilter = this._register(new Emitter()); + readonly onDidChangeFilter = this._onDidChangeFilter.event; private readonly _notifications: INotificationViewItem[] = []; get notifications(): INotificationViewItem[] { return this._notifications; } @@ -149,7 +154,7 @@ export class NotificationsModel extends Disposable implements INotificationsMode setFilter(filter: NotificationsFilter): void { this.filter = filter; - this._onDidFilterChange.fire(filter); + this._onDidChangeFilter.fire(filter); } addNotification(notification: INotification): INotificationHandle { @@ -168,13 +173,13 @@ export class NotificationsModel extends Disposable implements INotificationsMode this._notifications.splice(0, 0, item); // Events - this._onDidNotificationChange.fire({ item, index: 0, kind: NotificationChangeType.ADD }); + this._onDidChangeNotification.fire({ item, index: 0, kind: NotificationChangeType.ADD }); // Wrap into handle - return new NotificationHandle(item, item => this.closeItem(item)); + return new NotificationHandle(item, item => this.onClose(item)); } - private closeItem(item: INotificationViewItem): void { + private onClose(item: INotificationViewItem): void { const liveItem = this.findNotification(item); if (liveItem && liveItem !== item) { liveItem.close(); // item could have been replaced with another one, make sure to close the live item @@ -197,13 +202,13 @@ export class NotificationsModel extends Disposable implements INotificationsMode const onItemChangeEvent = () => { const index = this._notifications.indexOf(item); if (index >= 0) { - this._onDidNotificationChange.fire({ item, index, kind: NotificationChangeType.CHANGE }); + this._onDidChangeNotification.fire({ item, index, kind: NotificationChangeType.CHANGE }); } }; - const itemExpansionChangeListener = item.onDidExpansionChange(() => onItemChangeEvent()); + const itemExpansionChangeListener = item.onDidChangeExpansion(() => onItemChangeEvent()); - const itemLabelChangeListener = item.onDidLabelChange(e => { + const itemLabelChangeListener = item.onDidChangeLabel(e => { // a label change in the area of actions or the message is a change that potentially has an impact // on the size of the notification and as such we emit a change event so that viewers can redraw if (e.kind === NotificationViewItemLabelKind.ACTIONS || e.kind === NotificationViewItemLabelKind.MESSAGE) { @@ -218,7 +223,7 @@ export class NotificationsModel extends Disposable implements INotificationsMode const index = this._notifications.indexOf(item); if (index >= 0) { this._notifications.splice(index, 1); - this._onDidNotificationChange.fire({ item, index, kind: NotificationChangeType.REMOVE }); + this._onDidChangeNotification.fire({ item, index, kind: NotificationChangeType.REMOVE }); } }); @@ -233,14 +238,14 @@ export class NotificationsModel extends Disposable implements INotificationsMode // Remember as current status message and fire events this._statusMessage = item; - this._onDidStatusMessageChange.fire({ kind: StatusMessageChangeType.ADD, item }); + this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.ADD, item }); return toDisposable(() => { // Only reset status message if the item is still the one we had remembered if (this._statusMessage === item) { this._statusMessage = undefined; - this._onDidStatusMessageChange.fire({ kind: StatusMessageChangeType.REMOVE, item }); + this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.REMOVE, item }); } }); } @@ -258,9 +263,9 @@ export interface INotificationViewItem { readonly expanded: boolean; readonly canCollapse: boolean; - readonly onDidExpansionChange: Event; + readonly onDidChangeExpansion: Event; readonly onDidClose: Event; - readonly onDidLabelChange: Event; + readonly onDidChangeLabel: Event; expand(): void; collapse(skipEvents?: boolean): void; @@ -309,8 +314,8 @@ export interface INotificationViewItemProgress extends INotificationProgress { export class NotificationViewItemProgress extends Disposable implements INotificationViewItemProgress { private readonly _state: INotificationViewItemProgressState; - private readonly _onDidChange: Emitter = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; constructor() { super(); @@ -405,14 +410,14 @@ export class NotificationViewItem extends Disposable implements INotificationVie private _actions: INotificationActions | undefined; private _progress: NotificationViewItemProgress | undefined; - private readonly _onDidExpansionChange: Emitter = this._register(new Emitter()); - readonly onDidExpansionChange: Event = this._onDidExpansionChange.event; + private readonly _onDidChangeExpansion = this._register(new Emitter()); + readonly onDidChangeExpansion = this._onDidChangeExpansion.event; - private readonly _onDidClose: Emitter = this._register(new Emitter()); - readonly onDidClose: Event = this._onDidClose.event; + private readonly _onDidClose = this._register(new Emitter()); + readonly onDidClose = this._onDidClose.event; - private readonly _onDidLabelChange: Emitter = this._register(new Emitter()); - readonly onDidLabelChange: Event = this._onDidLabelChange.event; + private readonly _onDidChangeLabel = this._register(new Emitter()); + readonly onDidChangeLabel = this._onDidChangeLabel.event; static create(notification: INotification, filter: NotificationsFilter = NotificationsFilter.OFF): INotificationViewItem | undefined { if (!notification || !notification.message || isPromiseCanceledError(notification.message)) { @@ -561,7 +566,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie get progress(): INotificationViewItemProgress { if (!this._progress) { this._progress = this._register(new NotificationViewItemProgress()); - this._register(this._progress.onDidChange(() => this._onDidLabelChange.fire({ kind: NotificationViewItemLabelKind.PROGRESS }))); + this._register(this._progress.onDidChange(() => this._onDidChangeLabel.fire({ kind: NotificationViewItemLabelKind.PROGRESS }))); } return this._progress; @@ -581,7 +586,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie updateSeverity(severity: Severity): void { this._severity = severity; - this._onDidLabelChange.fire({ kind: NotificationViewItemLabelKind.SEVERITY }); + this._onDidChangeLabel.fire({ kind: NotificationViewItemLabelKind.SEVERITY }); } updateMessage(input: NotificationMessage): void { @@ -591,13 +596,13 @@ export class NotificationViewItem extends Disposable implements INotificationVie } this._message = message; - this._onDidLabelChange.fire({ kind: NotificationViewItemLabelKind.MESSAGE }); + this._onDidChangeLabel.fire({ kind: NotificationViewItemLabelKind.MESSAGE }); } updateActions(actions?: INotificationActions): void { this.setActions(actions); - this._onDidLabelChange.fire({ kind: NotificationViewItemLabelKind.ACTIONS }); + this._onDidChangeLabel.fire({ kind: NotificationViewItemLabelKind.ACTIONS }); } expand(): void { @@ -606,7 +611,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie } this._expanded = true; - this._onDidExpansionChange.fire(); + this._onDidChangeExpansion.fire(); } collapse(skipEvents?: boolean): void { @@ -617,7 +622,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie this._expanded = false; if (!skipEvents) { - this._onDidExpansionChange.fire(); + this._onDidChangeExpansion.fire(); } } @@ -656,8 +661,8 @@ export class NotificationViewItem extends Disposable implements INotificationVie export class ChoiceAction extends Action { - private readonly _onDidRun = new Emitter(); - readonly onDidRun: Event = this._onDidRun.event; + private readonly _onDidRun = this._register(new Emitter()); + readonly onDidRun = this._onDidRun.event; private readonly _keepOpen: boolean; @@ -679,12 +684,6 @@ export class ChoiceAction extends Action { get keepOpen(): boolean { return this._keepOpen; } - - dispose(): void { - super.dispose(); - - this._onDidRun.dispose(); - } } class StatusMessageViewItem { diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts index c509716fc499..408dcca11ac4 100644 --- a/src/vs/workbench/common/resources.ts +++ b/src/vs/workbench/common/resources.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import * as objects from 'vs/base/common/objects'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { basename, extname, relativePath } from 'vs/base/common/resources'; import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -106,8 +106,8 @@ export class ResourceGlobMatcher extends Disposable { private static readonly NO_ROOT: string | null = null; - private readonly _onExpressionChange: Emitter = this._register(new Emitter()); - readonly onExpressionChange: Event = this._onExpressionChange.event; + private readonly _onExpressionChange = this._register(new Emitter()); + readonly onExpressionChange = this._onExpressionChange.event; private readonly mapRootToParsedExpression: Map = new Map(); private readonly mapRootToExpressionConfig: Map = new Map(); diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 0a6073ca70b6..b171e8d1dcd3 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -57,12 +57,12 @@ export interface IViewContainerDescriptor { export interface IViewContainersRegistry { /** - * An event that is triggerred when a view container is registered. + * An event that is triggered when a view container is registered. */ readonly onDidRegister: Event<{ viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation }>; /** - * An event that is triggerred when a view container is deregistered. + * An event that is triggered when a view container is deregistered. */ readonly onDidDeregister: Event<{ viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation }>; @@ -213,6 +213,7 @@ export interface IViewDescriptorCollection extends IDisposable { export interface IViewContentDescriptor { readonly content: string; + readonly when?: ContextKeyExpr | 'default'; } export interface IViewsRegistry { @@ -235,9 +236,13 @@ export interface IViewsRegistry { getViewContainer(id: string): ViewContainer | null; - readonly onDidChangeEmptyViewContent: Event; - registerEmptyViewContent(id: string, viewContent: IViewContentDescriptor): IDisposable; - getEmptyViewContent(id: string): IViewContentDescriptor[]; + readonly onDidChangeViewWelcomeContent: Event; + registerViewWelcomeContent(id: string, viewContent: IViewContentDescriptor): IDisposable; + getViewWelcomeContent(id: string): IViewContentDescriptor[]; +} + +function compareViewContentDescriptors(a: IViewContentDescriptor, b: IViewContentDescriptor): number { + return a.content < b.content ? -1 : 1; } class ViewsRegistry extends Disposable implements IViewsRegistry { @@ -251,12 +256,12 @@ class ViewsRegistry extends Disposable implements IViewsRegistry { private readonly _onDidChangeContainer: Emitter<{ views: IViewDescriptor[], from: ViewContainer, to: ViewContainer }> = this._register(new Emitter<{ views: IViewDescriptor[], from: ViewContainer, to: ViewContainer }>()); readonly onDidChangeContainer: Event<{ views: IViewDescriptor[], from: ViewContainer, to: ViewContainer }> = this._onDidChangeContainer.event; - private readonly _onDidChangeEmptyViewContent: Emitter = this._register(new Emitter()); - readonly onDidChangeEmptyViewContent: Event = this._onDidChangeEmptyViewContent.event; + private readonly _onDidChangeViewWelcomeContent: Emitter = this._register(new Emitter()); + readonly onDidChangeViewWelcomeContent: Event = this._onDidChangeViewWelcomeContent.event; private _viewContainers: ViewContainer[] = []; private _views: Map = new Map(); - private _emptyViewContents = new SetMap(); + private _viewWelcomeContents = new SetMap(); registerViews(views: IViewDescriptor[], viewContainer: ViewContainer): void { this.addViews(views, viewContainer); @@ -306,19 +311,20 @@ class ViewsRegistry extends Disposable implements IViewsRegistry { return null; } - registerEmptyViewContent(id: string, viewContent: IViewContentDescriptor): IDisposable { - this._emptyViewContents.add(id, viewContent); - this._onDidChangeEmptyViewContent.fire(id); + registerViewWelcomeContent(id: string, viewContent: IViewContentDescriptor): IDisposable { + this._viewWelcomeContents.add(id, viewContent); + this._onDidChangeViewWelcomeContent.fire(id); return toDisposable(() => { - this._emptyViewContents.delete(id, viewContent); - this._onDidChangeEmptyViewContent.fire(id); + this._viewWelcomeContents.delete(id, viewContent); + this._onDidChangeViewWelcomeContent.fire(id); }); } - getEmptyViewContent(id: string): IViewContentDescriptor[] { + getViewWelcomeContent(id: string): IViewContentDescriptor[] { const result: IViewContentDescriptor[] = []; - this._emptyViewContents.forEach(id, descriptor => result.push(descriptor)); + result.sort(compareViewContentDescriptors); + this._viewWelcomeContents.forEach(id, descriptor => result.push(descriptor)); return result; } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts index 2d0de4bbb3d3..0970caecdbb0 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts @@ -6,7 +6,7 @@ import 'vs/css!./bulkEdit'; import { WorkbenchAsyncDataTree, TreeResourceNavigator, IOpenEvent } from 'vs/platform/list/browser/listService'; import { WorkspaceEdit } from 'vs/editor/common/modes'; -import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, BulkEditAriaProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditTree'; +import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, BulkEditAriaProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement, BulkEditSorter } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditTree'; import { FuzzyScore } from 'vs/base/common/filters'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { registerThemingParticipant, ITheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; @@ -135,6 +135,7 @@ export class BulkEditPane extends ViewPane { expandOnlyOnTwistieClick: true, multipleSelectionSupport: false, keyboardNavigationLabelProvider: new BulkEditNaviLabelProvider(), + sorter: new BulkEditSorter() } ); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts index 8d7ec503497d..06bdb85376ac 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts @@ -99,6 +99,15 @@ export class BulkFileOperation { this.newUri = edit.newUri; } } + + needsConfirmation(): boolean { + for (let [, edit] of this.originalEdits) { + if (!this.parent.checked.isChecked(edit)) { + return true; + } + } + return false; + } } export class BulkCategory { @@ -230,7 +239,7 @@ export class BulkFileOperations { } operationByResource.forEach(value => this.fileOperations.push(value)); - operationByCategory.forEach(value => value.metadata.needsConfirmation ? this.categories.unshift(value) : this.categories.push(value)); + operationByCategory.forEach(value => this.categories.push(value)); // "correct" invalid parent-check child states that is // unchecked file edits (rename, create, delete) uncheck diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts index 4a50a0b6b890..544fa1027d4d 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAsyncDataSource, ITreeRenderer, ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { IAsyncDataSource, ITreeRenderer, ITreeNode, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; @@ -24,6 +24,7 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { basename } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { WorkspaceFileEdit } from 'vs/editor/common/modes'; +import { compare } from 'vs/base/common/strings'; // --- VIEW MODEL @@ -248,6 +249,39 @@ export class BulkEditDataSource implements IAsyncDataSource { + + compare(a: BulkEditElement, b: BulkEditElement): number { + if (a instanceof CategoryElement && b instanceof CategoryElement) { + // + const aConfirm = BulkEditSorter._needsConfirmation(a.category); + const bConfirm = BulkEditSorter._needsConfirmation(b.category); + if (aConfirm === bConfirm) { + return a.category.metadata.label.localeCompare(b.category.metadata.label); + } else if (aConfirm) { + return -1; + } else { + return 1; + } + } + + if (a instanceof FileElement && b instanceof FileElement) { + return compare(a.edit.uri.toString(), b.edit.uri.toString()); + } + + if (a instanceof TextEditElement && b instanceof TextEditElement) { + return Range.compareRangesUsingStarts(a.edit.textEdit.edit.range, b.edit.textEdit.edit.range); + } + + return 0; + } + + private static _needsConfirmation(a: BulkCategory): boolean { + return a.fileOperations.some(ops => ops.needsConfirmation()); + } +} + // --- ACCESSI export class BulkEditAccessibilityProvider implements IAccessibilityProvider { diff --git a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts index dc8c179721c4..49cf081d9675 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts @@ -9,6 +9,7 @@ import './diffEditorHelper'; import './inspectKeybindings'; import './largeFileOptimizations'; import './inspectEditorTokens/inspectEditorTokens'; +import './saveParticipants'; import './toggleMinimap'; import './toggleMultiCursorModifier'; import './toggleRenderControlCharacter'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts new file mode 100644 index 000000000000..ea6fc68420f3 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts @@ -0,0 +1,316 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import * as strings from 'vs/base/common/strings'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ITextModel } from 'vs/editor/common/model'; +import { CodeAction, CodeActionTriggerType } from 'vs/editor/common/modes'; +import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; +import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; +import { CodeActionKind } from 'vs/editor/contrib/codeAction/types'; +import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; +import { IResolvedTextFileEditorModel, ITextFileService, ITextFileSaveParticipant } from 'vs/workbench/services/textfile/common/textfiles'; +import { SaveReason } from 'vs/workbench/common/editor'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution, Extensions as WorkbenchContributionsExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; + +class TrimWhitespaceParticipant implements ITextFileSaveParticipant { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService + ) { + // Nothing + } + + async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { + if (this.configurationService.getValue('files.trimTrailingWhitespace', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { + this.doTrimTrailingWhitespace(model.textEditorModel, env.reason === SaveReason.AUTO); + } + } + + private doTrimTrailingWhitespace(model: ITextModel, isAutoSaved: boolean): void { + let prevSelection: Selection[] = []; + let cursors: Position[] = []; + + const editor = findEditor(model, this.codeEditorService); + if (editor) { + // Find `prevSelection` in any case do ensure a good undo stack when pushing the edit + // Collect active cursors in `cursors` only if `isAutoSaved` to avoid having the cursors jump + prevSelection = editor.getSelections(); + if (isAutoSaved) { + cursors = prevSelection.map(s => s.getPosition()); + const snippetsRange = SnippetController2.get(editor).getSessionEnclosingRange(); + if (snippetsRange) { + for (let lineNumber = snippetsRange.startLineNumber; lineNumber <= snippetsRange.endLineNumber; lineNumber++) { + cursors.push(new Position(lineNumber, model.getLineMaxColumn(lineNumber))); + } + } + } + } + + const ops = trimTrailingWhitespace(model, cursors); + if (!ops.length) { + return; // Nothing to do + } + + model.pushEditOperations(prevSelection, ops, (_edits) => prevSelection); + } +} + +function findEditor(model: ITextModel, codeEditorService: ICodeEditorService): IActiveCodeEditor | null { + let candidate: IActiveCodeEditor | null = null; + + if (model.isAttachedToEditor()) { + for (const editor of codeEditorService.listCodeEditors()) { + if (editor.hasModel() && editor.getModel() === model) { + if (editor.hasTextFocus()) { + return editor; // favour focused editor if there are multiple + } + + candidate = editor; + } + } + } + + return candidate; +} + +export class FinalNewLineParticipant implements ITextFileSaveParticipant { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService + ) { + // Nothing + } + + async participate(model: IResolvedTextFileEditorModel, _env: { reason: SaveReason; }): Promise { + if (this.configurationService.getValue('files.insertFinalNewline', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { + this.doInsertFinalNewLine(model.textEditorModel); + } + } + + private doInsertFinalNewLine(model: ITextModel): void { + const lineCount = model.getLineCount(); + const lastLine = model.getLineContent(lineCount); + const lastLineIsEmptyOrWhitespace = strings.lastNonWhitespaceIndex(lastLine) === -1; + + if (!lineCount || lastLineIsEmptyOrWhitespace) { + return; + } + + const edits = [EditOperation.insert(new Position(lineCount, model.getLineMaxColumn(lineCount)), model.getEOL())]; + const editor = findEditor(model, this.codeEditorService); + if (editor) { + editor.executeEdits('insertFinalNewLine', edits, editor.getSelections()); + } else { + model.pushEditOperations([], edits, () => null); + } + } +} + +export class TrimFinalNewLinesParticipant implements ITextFileSaveParticipant { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService + ) { + // Nothing + } + + async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { + if (this.configurationService.getValue('files.trimFinalNewlines', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { + this.doTrimFinalNewLines(model.textEditorModel, env.reason === SaveReason.AUTO); + } + } + + /** + * returns 0 if the entire file is empty or whitespace only + */ + private findLastLineWithContent(model: ITextModel): number { + for (let lineNumber = model.getLineCount(); lineNumber >= 1; lineNumber--) { + const lineContent = model.getLineContent(lineNumber); + if (strings.lastNonWhitespaceIndex(lineContent) !== -1) { + // this line has content + return lineNumber; + } + } + // no line has content + return 0; + } + + private doTrimFinalNewLines(model: ITextModel, isAutoSaved: boolean): void { + const lineCount = model.getLineCount(); + + // Do not insert new line if file does not end with new line + if (lineCount === 1) { + return; + } + + let prevSelection: Selection[] = []; + let cannotTouchLineNumber = 0; + const editor = findEditor(model, this.codeEditorService); + if (editor) { + prevSelection = editor.getSelections(); + if (isAutoSaved) { + for (let i = 0, len = prevSelection.length; i < len; i++) { + const positionLineNumber = prevSelection[i].positionLineNumber; + if (positionLineNumber > cannotTouchLineNumber) { + cannotTouchLineNumber = positionLineNumber; + } + } + } + } + + const lastLineNumberWithContent = this.findLastLineWithContent(model); + const deleteFromLineNumber = Math.max(lastLineNumberWithContent + 1, cannotTouchLineNumber + 1); + const deletionRange = model.validateRange(new Range(deleteFromLineNumber, 1, lineCount, model.getLineMaxColumn(lineCount))); + + if (deletionRange.isEmpty()) { + return; + } + + model.pushEditOperations(prevSelection, [EditOperation.delete(deletionRange)], _edits => prevSelection); + + if (editor) { + editor.setSelections(prevSelection); + } + } +} + +class FormatOnSaveParticipant implements ITextFileSaveParticipant { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + // Nothing + } + + async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, progress: IProgress, token: CancellationToken): Promise { + const model = editorModel.textEditorModel; + const overrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: model.uri }; + + if (env.reason === SaveReason.AUTO || !this.configurationService.getValue('editor.formatOnSave', overrides)) { + return undefined; + } + + progress.report({ message: localize('formatting', "Formatting") }); + const editorOrModel = findEditor(model, this.codeEditorService) || model; + await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, token); + } +} + +class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { } + + async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, progress: IProgress, token: CancellationToken): Promise { + if (env.reason === SaveReason.AUTO) { + return undefined; + } + const model = editorModel.textEditorModel; + + const settingsOverrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: editorModel.resource }; + const setting = this.configurationService.getValue<{ [kind: string]: boolean }>('editor.codeActionsOnSave', settingsOverrides); + if (!setting) { + return undefined; + } + + const codeActionsOnSave = Object.keys(setting) + .filter(x => setting[x]).map(x => new CodeActionKind(x)) + .sort((a, b) => { + if (CodeActionKind.SourceFixAll.contains(a)) { + if (CodeActionKind.SourceFixAll.contains(b)) { + return 0; + } + return -1; + } + if (CodeActionKind.SourceFixAll.contains(b)) { + return 1; + } + return 0; + }); + + if (!codeActionsOnSave.length) { + return undefined; + } + + const excludedActions = Object.keys(setting) + .filter(x => setting[x] === false) + .map(x => new CodeActionKind(x)); + + progress.report({ message: localize('codeaction', "Quick Fixes") }); + await this.applyOnSaveActions(model, codeActionsOnSave, excludedActions, token); + } + + private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly CodeActionKind[], excludes: readonly CodeActionKind[], token: CancellationToken): Promise { + for (const codeActionKind of codeActionsOnSave) { + const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, token); + try { + await this.applyCodeActions(actionsToRun.validActions); + } catch { + // Failure to apply a code action should not block other on save actions + } finally { + actionsToRun.dispose(); + } + } + } + + private async applyCodeActions(actionsToRun: readonly CodeAction[]) { + for (const action of actionsToRun) { + await this.instantiationService.invokeFunction(applyCodeAction, action); + } + } + + private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, excludes: readonly CodeActionKind[], token: CancellationToken) { + return getCodeActions(model, model.getFullModelRange(), { + type: CodeActionTriggerType.Auto, + filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true }, + }, token); + } +} + +export class SaveParticipantsContribution extends Disposable implements IWorkbenchContribution { + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ITextFileService private readonly textFileService: ITextFileService + ) { + super(); + + this.registerSaveParticipants(); + } + + private registerSaveParticipants(): void { + this._register(this.textFileService.files.addSaveParticipant(this.instantiationService.createInstance(TrimWhitespaceParticipant))); + this._register(this.textFileService.files.addSaveParticipant(this.instantiationService.createInstance(CodeActionOnSaveParticipant))); + this._register(this.textFileService.files.addSaveParticipant(this.instantiationService.createInstance(FormatOnSaveParticipant))); + this._register(this.textFileService.files.addSaveParticipant(this.instantiationService.createInstance(FinalNewLineParticipant))); + this._register(this.textFileService.files.addSaveParticipant(this.instantiationService.createInstance(TrimFinalNewLinesParticipant))); + } +} + +const workbenchContributionsRegistry = Registry.as(WorkbenchContributionsExtensions.Workbench); +workbenchContributionsRegistry.registerWorkbenchContribution(SaveParticipantsContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts similarity index 98% rename from src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts rename to src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts index e9c8770111ec..c695bd3eaefe 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts @@ -5,9 +5,9 @@ import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { FinalNewLineParticipant, TrimFinalNewLinesParticipant } from 'vs/workbench/api/browser/mainThreadSaveParticipant'; +import { FinalNewLineParticipant, TrimFinalNewLinesParticipant } from 'vs/workbench/contrib/codeEditor/browser/saveParticipants'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; +import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/browser/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; import { IModelService } from 'vs/editor/common/services/modelService'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts index 6814383e70b8..71c474a0b15d 100644 --- a/src/vs/workbench/contrib/customEditor/browser/commands.ts +++ b/src/vs/workbench/contrib/customEditor/browser/commands.ts @@ -40,7 +40,7 @@ CommandsRegistry.registerCommand('_workbench.openWith', (accessor: ServicesAcces // #region Reopen With const REOPEN_WITH_COMMAND_ID = 'reOpenWith'; -const REOPEN_WITH_TITLE = { value: nls.localize('reopenWith.title', 'Reopen With'), original: 'Reopen With' }; +const REOPEN_WITH_TITLE = { value: nls.localize('reopenWith.title', 'Reopen With...'), original: 'Reopen With' }; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: REOPEN_WITH_COMMAND_ID, @@ -83,6 +83,17 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { when: CONTEXT_HAS_CUSTOM_EDITORS, }); +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: REOPEN_WITH_COMMAND_ID, + title: REOPEN_WITH_TITLE, + category: viewCategory, + }, + group: '3_open', + order: 20, + when: CONTEXT_HAS_CUSTOM_EDITORS, +}); + // #endregion diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index 3b75c468a51c..5d4be69a867c 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -19,7 +19,6 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel'; import { once } from 'vs/base/common/functional'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; export const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; export const twistiePixels = 20; @@ -233,11 +232,3 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer; private needsRefresh = false; @@ -82,6 +81,7 @@ export class BreakpointsView extends BaseDebugViewPane { public renderBody(container: HTMLElement): void { super.renderBody(container); + dom.addClass(this.element, 'debug-pane'); dom.addClass(container, 'debug-breakpoints'); const delegate = new BreakpointsDelegate(this.debugService); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 805bac9c9166..5ae2c952efe1 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -13,12 +13,12 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MenuId, IMenu, IMenuService } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { renderViewTree, BaseDebugViewPane } from 'vs/workbench/contrib/debug/browser/baseDebugView'; +import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IAction, Action } from 'vs/base/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { ILabelService } from 'vs/platform/label/common/label'; import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -74,7 +74,7 @@ export function getContextForContributedActions(element: CallStackItem | null): return ''; } -export class CallStackView extends BaseDebugViewPane { +export class CallStackView extends ViewPane { private pauseMessage!: HTMLSpanElement; private pauseMessageLabel!: HTMLSpanElement; private onCallStackChangeScheduler: RunOnceScheduler; @@ -158,7 +158,7 @@ export class CallStackView extends BaseDebugViewPane { renderBody(container: HTMLElement): void { super.renderBody(container); - + dom.addClass(this.element, 'debug-pane'); dom.addClass(container, 'debug-call-stack'); const treeContainer = renderViewTree(container); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index c22007db2f24..a8cba2d296bf 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -136,17 +136,19 @@ function getWordToLineNumbersMap(model: ITextModel | null): Map { - const pos = range.getStartPosition(); const session = this.debugService.getViewModel().focusedSession; - if (!this.editor.hasModel()) { + + if (!session || !this.editor.hasModel()) { return Promise.resolve(this.hide()); } - const lineContent = this.editor.getModel().getLineContent(pos.lineNumber); - const { start, end } = getExactExpressionStartAndEnd(lineContent, range.startColumn, range.endColumn); - // use regex to extract the sub-expression #9821 - const matchingExpression = lineContent.substring(start - 1, end); - if (!matchingExpression || !session) { + const model = this.editor.getModel(); + const pos = range.getStartPosition(); + + let rng: IRange | undefined = undefined; + let matchingExpression: string | undefined; + + if (EvaluatableExpressionProviderRegistry.has(model)) { + const supports = EvaluatableExpressionProviderRegistry.ordered(model); + + const promises = supports.map(support => { + return Promise.resolve(support.provideEvaluatableExpression(model, pos, CancellationToken.None)).then(expression => { + return expression; + }, err => { + //onUnexpectedExternalError(err); + return undefined; + }); + }); + + const results = await Promise.all(promises).then(coalesce); + if (results.length > 0) { + matchingExpression = results[0].expression; + rng = results[0].range; + + if (!matchingExpression) { + const lineContent = model.getLineContent(pos.lineNumber); + matchingExpression = lineContent.substring(rng.startColumn - 1, rng.endColumn); + } + } + + } else { // old one-size-fits-all strategy + const lineContent = model.getLineContent(pos.lineNumber); + const { start, end } = getExactExpressionStartAndEnd(lineContent, range.startColumn, range.endColumn); + + // use regex to extract the sub-expression #9821 + matchingExpression = lineContent.substring(start - 1, end); + rng = new Range(pos.lineNumber, start, pos.lineNumber, start + matchingExpression.length); + } + + if (!matchingExpression) { return Promise.resolve(this.hide()); } @@ -202,13 +239,15 @@ export class DebugHoverWidget implements IContentWidget { if (!expression || (expression instanceof Expression && !expression.available)) { this.hide(); - return undefined; + return; } - this.highlightDecorations = this.editor.deltaDecorations(this.highlightDecorations, [{ - range: new Range(pos.lineNumber, start, pos.lineNumber, start + matchingExpression.length), - options: DebugHoverWidget._HOVER_HIGHLIGHT_DECORATION_OPTIONS - }]); + if (rng) { + this.highlightDecorations = this.editor.deltaDecorations(this.highlightDecorations, [{ + range: rng, + options: DebugHoverWidget._HOVER_HIGHLIGHT_DECORATION_OPTIONS + }]); + } return this.doShow(pos, expression, focus); } diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index bf56471c4a41..ceb03abfcc97 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -7,12 +7,12 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { normalize, isAbsolute, posix } from 'vs/base/common/path'; -import { IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { renderViewTree, BaseDebugViewPane } from 'vs/workbench/contrib/debug/browser/baseDebugView'; +import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IDebugSession, IDebugService, CONTEXT_LOADED_SCRIPTS_ITEM_TYPE } from 'vs/workbench/contrib/debug/common/debug'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -402,7 +402,7 @@ function asTreeElement(item: BaseTreeItem, viewState?: IViewState): ITreeElement }; } -export class LoadedScriptsView extends BaseDebugViewPane { +export class LoadedScriptsView extends ViewPane { private treeContainer!: HTMLElement; private loadedScriptsItemType: IContextKey; @@ -435,6 +435,7 @@ export class LoadedScriptsView extends BaseDebugViewPane { renderBody(container: HTMLElement): void { super.renderBody(container); + dom.addClass(this.element, 'debug-pane'); dom.addClass(container, 'debug-loaded-scripts'); dom.addClass(container, 'show-file-icons'); diff --git a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css index 12a93e85e7d9..2fb7b93bd76c 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css @@ -75,7 +75,7 @@ /* Expressions */ -.monaco-workbench .debug-viewlet .monaco-list-row .expression, +.monaco-workbench .debug-pane .monaco-list-row .expression, .monaco-workbench .debug-hover-widget .monaco-list-row .expression { font-size: 13px; overflow: hidden; @@ -84,7 +84,7 @@ white-space: pre; } -.monaco-workbench.mac .debug-viewlet .monaco-list-row .expression, +.monaco-workbench.mac .debug-pane .monaco-list-row .expression, .monaco-workbench.mac .debug-hover-widget .monaco-list-row .expression { font-size: 11px; } diff --git a/src/vs/workbench/contrib/debug/browser/media/debugHover.css b/src/vs/workbench/contrib/debug/browser/media/debugHover.css index b01a3d1193d1..a0406499aa3c 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugHover.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugHover.css @@ -12,7 +12,6 @@ user-select: text; -webkit-user-select: text; word-break: break-all; - padding: 4px 5px; } .monaco-editor .debug-hover-widget .complex-value { @@ -62,6 +61,7 @@ overflow: auto; font-family: var(--monaco-monospace-font); max-height: 500px; + padding: 4px 5px; } .monaco-editor .debug-hover-widget .error { diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index f751bebda623..da729176ce97 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -601,7 +601,7 @@ export class RawDebugSession implements IDisposable { private send(command: string, args: any, token?: CancellationToken, timeout?: number): Promise { return new Promise((completeDispatch, errorDispatch) => { if (!this.debugAdapter) { - errorDispatch(new Error('no debug adapter found')); + errorDispatch(new Error(nls.localize('noDebugAdapter', "No debug adapter found. Can not send '{0}'.", command))); return; } let cancelationListener: IDisposable; diff --git a/src/vs/workbench/contrib/debug/browser/startView.ts b/src/vs/workbench/contrib/debug/browser/startView.ts index 923462eee949..f561b8cc42b1 100644 --- a/src/vs/workbench/contrib/debug/browser/startView.ts +++ b/src/vs/workbench/contrib/debug/browser/startView.ts @@ -20,7 +20,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { equals } from 'vs/base/common/arrays'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -170,6 +170,7 @@ export class StartView extends ViewPane { })); attachButtonStyler(this.debugButton, this.themeService); + dom.addClass(this.element, 'debug-pane'); dom.addClass(container, 'debug-start-view'); this.secondMessageContainer = $('.section'); diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 8e9dd8dc2b05..4dd9c4bfac09 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -12,12 +12,12 @@ import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IViewMod import { Variable, Scope } from 'vs/workbench/contrib/debug/common/debugModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { renderViewTree, renderVariable, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData, BaseDebugViewPane } from 'vs/workbench/contrib/debug/browser/baseDebugView'; +import { renderViewTree, renderVariable, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IAction, Action } from 'vs/base/common/actions'; import { CopyValueAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, ITreeMouseEvent, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; @@ -39,7 +39,7 @@ let forgetScopes = true; export const variableSetEmitter = new Emitter(); -export class VariablesView extends BaseDebugViewPane { +export class VariablesView extends ViewPane { private onFocusStackFrameScheduler: RunOnceScheduler; private needsRefresh = false; @@ -90,6 +90,7 @@ export class VariablesView extends BaseDebugViewPane { renderBody(container: HTMLElement): void { super.renderBody(container); + dom.addClass(this.element, 'debug-pane'); dom.addClass(container, 'debug-variables'); const treeContainer = renderViewTree(container); diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index df6770a121f2..99b9cc6d32e6 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -16,9 +16,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IAction, Action } from 'vs/base/common/actions'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { renderExpressionValue, renderViewTree, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData, BaseDebugViewPane } from 'vs/workbench/contrib/debug/browser/baseDebugView'; +import { renderExpressionValue, renderViewTree, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; @@ -38,7 +38,7 @@ const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; let ignoreVariableSetEmitter = false; let useCachedEvaluation = false; -export class WatchExpressionsView extends BaseDebugViewPane { +export class WatchExpressionsView extends ViewPane { private onWatchExpressionsUpdatedScheduler: RunOnceScheduler; private needsRefresh = false; @@ -67,6 +67,7 @@ export class WatchExpressionsView extends BaseDebugViewPane { renderBody(container: HTMLElement): void { super.renderBody(container); + dom.addClass(this.element, 'debug-pane'); dom.addClass(container, 'debug-watch'); const treeContainer = renderViewTree(container); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 3228cb72d0c7..49ffa58bb985 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -14,7 +14,7 @@ import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } fro import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { VIEWLET_ID, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, @@ -47,6 +47,7 @@ import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewConta import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -290,6 +291,30 @@ CommandsRegistry.registerCommand({ } }); +CommandsRegistry.registerCommand({ + id: 'workbench.extensions.search', + description: { + description: localize('workbench.extensions.search.description', "Search for a specific extension"), + args: [ + { + name: localize('workbench.extensions.search.arg.name', "Query to use in search"), + schema: { 'type': 'string' } + } + ] + }, + handler: async (accessor, query: string = '') => { + const viewletService = accessor.get(IViewletService); + const viewlet = await viewletService.openViewlet(VIEWLET_ID, true); + + if (!viewlet) { + return; + } + + (viewlet.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query); + viewlet.focus(); + } +}); + // File menu registration MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 5c214b0641df..af798c52dd85 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -64,7 +64,7 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa this._register(textModelService.registerTextModelContentProvider(CONFLICT_RESOLUTION_SCHEME, provider)); // Set as save error handler to service for text files - this.textFileService.saveErrorHandler = this; + this.textFileService.files.saveErrorHandler = this; this.registerListeners(); } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 806bbb4fda1c..1af95f4a23c8 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -676,7 +676,11 @@ export class ExplorerView extends ViewPane { if (item.isDisposed) { return this.onSelectResource(resource, reveal, retry + 1); } - this.tree.reveal(item, 0.5); + + // Don't scroll to the item if it's already visible + if (this.tree.getRelativeTop(item) === null) { + this.tree.reveal(item, 0.5); + } } this.tree.setFocus([item]); diff --git a/src/vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution.ts b/src/vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution.ts index 0b959f3abd43..9854d0f0628f 100644 --- a/src/vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution.ts +++ b/src/vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution.ts @@ -29,13 +29,13 @@ export class OpenInDesktopIndicator extends Disposable implements IWorkbenchCont ) { super(); - const links = environmentService.options?.applicationLinkProvider?.(); + const links = environmentService.options?.applicationLinks; if (Array.isArray(links) && links?.length > 0) { this.installOpenInDesktopIndicator(links); } } - private installOpenInDesktopIndicator(links: IApplicationLink[]): void { + private installOpenInDesktopIndicator(links: readonly IApplicationLink[]): void { // Register action to trigger "Open In Desktop" const registry = Registry.as(ActionExtensions.WorkbenchActions); @@ -71,7 +71,7 @@ export class OpenInDesktopAction extends Action { } async run(): Promise { - const links = this.environmentService.options?.applicationLinkProvider?.(); + const links = this.environmentService.options?.applicationLinks; if (Array.isArray(links)) { if (links.length === 1) { return this.openApplicationLink(links[0]); @@ -83,7 +83,7 @@ export class OpenInDesktopAction extends Action { return true; } - private async runWithPicker(links: IApplicationLink[]): Promise { + private async runWithPicker(links: readonly IApplicationLink[]): Promise { // Show a picker with choices const quickPick = this.quickInputService.createQuickPick(); diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 7214dddd208c..c4ba64d1e82c 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -27,7 +27,7 @@ import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IResourceInput, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; +import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { WorkbenchDataTree } from 'vs/platform/list/browser/listService'; @@ -37,7 +37,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { CollapseAction } from 'vs/workbench/browser/viewlet'; -import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { OutlineConfigKeys, OutlineViewFocused, OutlineViewFiltered } from 'vs/editor/contrib/documentSymbols/outline'; import { FuzzyScore } from 'vs/base/common/filters'; import { OutlineDataSource, OutlineItemComparator, OutlineSortOrder, OutlineVirtualDelegate, OutlineGroupRenderer, OutlineElementRenderer, OutlineItem, OutlineIdentityProvider, OutlineNavigationLabelProvider, OutlineFilter } from 'vs/editor/contrib/documentSymbols/outlineTree'; @@ -49,6 +49,7 @@ import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDeco import { MarkerSeverity } from 'vs/platform/markers/common/markers'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; class RequestState { @@ -261,7 +262,7 @@ export class OutlinePane extends ViewPane { @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IThemeService private readonly _themeService: IThemeService, @IStorageService private readonly _storageService: IStorageService, - @IEditorService private readonly _editorService: IEditorService, + @ICodeEditorService private readonly _editorService: ICodeEditorService, @IMarkerDecorationsService private readonly _markerDecorationService: IMarkerDecorationsService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService, @@ -347,7 +348,7 @@ export class OutlinePane extends ViewPane { this._disposables.push(this._tree); this._disposables.push(this._outlineViewState.onDidChange(this._onDidChangeUserState, this)); - this._disposables.push(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { + this._disposables.push(this.viewDescriptorService.onDidChangeLocation(({ views }) => { if (views.some(v => v.id === this.id)) { this._tree.updateOptions({ overrideStyles: { listBackground: this.getBackgroundColor() } }); } @@ -629,15 +630,18 @@ export class OutlinePane extends ViewPane { } private async _revealTreeSelection(model: OutlineModel, element: OutlineElement, focus: boolean, aside: boolean): Promise { - - await this._editorService.openEditor({ - resource: model.textModel.uri, - options: { - preserveFocus: !focus, - selection: Range.collapseToStart(element.symbol.selectionRange), - selectionRevealType: TextEditorSelectionRevealType.NearTop, - } - } as IResourceInput, aside ? SIDE_GROUP : ACTIVE_GROUP); + await this._editorService.openCodeEditor( + { + resource: model.textModel.uri, + options: { + preserveFocus: !focus, + selection: Range.collapseToStart(element.symbol.selectionRange), + selectionRevealType: TextEditorSelectionRevealType.NearTop, + } + }, + this._editorService.getActiveCodeEditor(), + aside + ); } private _revealEditorSelection(model: OutlineModel, selection: Selection): void { diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts index 60bcbfd4ba48..a8865d6ec602 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts @@ -29,7 +29,7 @@ import { SelectionHighlighter } from 'vs/editor/contrib/multicursor/multicursor' import * as nls from 'vs/nls'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -984,7 +984,7 @@ export class DefaultPreferencesEditor extends BaseTextEditor { private static _getContributions(): IEditorContributionDescription[] { const skipContributions = [FoldingController.ID, SelectionHighlighter.ID, FindController.ID]; const contributions = EditorExtensionsRegistry.getEditorContributions().filter(c => skipContributions.indexOf(c.id) === -1); - contributions.push({ id: DefaultSettingsEditorContribution.ID, ctor: DefaultSettingsEditorContribution }); + contributions.push({ id: DefaultSettingsEditorContribution.ID, ctor: DefaultSettingsEditorContribution as IConstructorSignature1 }); return contributions; } diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index a152b50423d7..8ff6aff23437 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -15,7 +15,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { RunOnceScheduler } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; import { isEqual } from 'vs/base/common/resources'; -import { isMacintosh, isNative } from 'vs/base/common/platform'; +import { isMacintosh, isNative, isLinux } from 'vs/base/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -26,6 +26,7 @@ interface IConfiguration extends IWindowsConfiguration { telemetry: { enableCrashReporter: boolean }; workbench: { list: { horizontalScrolling: boolean } }; debug: { console: { wordWrap: boolean } }; + editor: { accessibilitySupport: 'on' | 'off' | 'auto' }; } export class SettingsChangeRelauncher extends Disposable implements IWorkbenchContribution { @@ -38,6 +39,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private enableCrashReporter: boolean | undefined; private treeHorizontalScrolling: boolean | undefined; private debugConsoleWordWrap: boolean | undefined; + private accessibilitySupport: 'on' | 'off' | 'auto' | undefined; constructor( @IHostService private readonly hostService: IHostService, @@ -103,6 +105,14 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo this.enableCrashReporter = config.telemetry.enableCrashReporter; changed = true; } + + // On linux turning on accessibility support will also pass this flag to the chrome renderer, thus a restart is required + if (isLinux && typeof config.editor?.accessibilitySupport === 'string' && config.editor.accessibilitySupport !== this.accessibilitySupport) { + this.accessibilitySupport = config.editor.accessibilitySupport; + if (this.accessibilitySupport === 'on') { + changed = true; + } + } } // Notify only when changed and we are the focused window (avoids notification spam across windows) diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index 482b66fb18d9..9c94a5c49eba 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css @@ -8,17 +8,6 @@ flex: 1; } -.scm-viewlet .empty-message { - box-sizing: border-box; - height: 100%; - padding: 10px 22px 0 22px; -} - -.scm-viewlet:not(.empty) .empty-message, -.scm-viewlet.empty .monaco-pane-view { - display: none; -} - .scm-viewlet .scm-status { height: 100%; position: relative; diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts index 3f65b434ea8c..ab9c041d1259 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts @@ -615,7 +615,7 @@ export class RepositoryPane extends ViewPane { protected contextKeyService: IContextKeyService; private commitTemplate = ''; - isEmpty() { return true; } + shouldShowWelcome() { return true; } constructor( readonly repository: ISCMRepository, diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index 128c0029152f..035f4617e542 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -6,12 +6,11 @@ import 'vs/css!./media/scmViewlet'; import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { append, $, toggleClass, addClasses } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { VIEWLET_ID, ISCMService, ISCMRepository } from 'vs/workbench/contrib/scm/common/scm'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MenuItemAction } from 'vs/platform/actions/common/actions'; @@ -25,13 +24,16 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IViewsRegistry, Extensions, IViewDescriptorService } from 'vs/workbench/common/views'; +import { IViewsRegistry, Extensions, IViewDescriptorService, IViewDescriptor } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { RepositoryPane, RepositoryViewDescriptor } from 'vs/workbench/contrib/scm/browser/repositoryPane'; import { MainPaneDescriptor, MainPane, IViewModel } from 'vs/workbench/contrib/scm/browser/mainPane'; -import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPaneContainer, IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import type { IAddedViewDescriptorRef, IViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; import { debounce } from 'vs/base/common/decorators'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { addClass } from 'vs/base/browser/dom'; export interface ISpliceEvent { index: number; @@ -39,12 +41,45 @@ export interface ISpliceEvent { elements: T[]; } +export class EmptyPane extends ViewPane { + + static readonly ID = 'workbench.scm'; + static readonly TITLE = localize('scm providers', "Source Control Providers"); + + constructor( + options: IViewPaneOptions, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IInstantiationService instantiationService: IInstantiationService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + } + + shouldShowWelcome(): boolean { + return true; + } +} + +export class EmptyPaneDescriptor implements IViewDescriptor { + readonly id = EmptyPane.ID; + readonly name = EmptyPane.TITLE; + readonly ctorDescriptor = new SyncDescriptor(EmptyPane); + readonly canToggleVisibility = true; + readonly hideByDefault = false; + readonly order = -1000; + readonly workspace = true; + readonly when = ContextKeyExpr.equals('scm.providerCount', 0); +} + export class SCMViewPaneContainer extends ViewPaneContainer implements IViewModel { private static readonly STATE_KEY = 'workbench.scm.views.state'; - private el!: HTMLElement; - private message: HTMLElement; private menus: SCMMenus; private _repositories: ISCMRepository[] = []; @@ -94,9 +129,14 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode this.menus = instantiationService.createInstance(SCMMenus, undefined); this._register(this.menus.onDidChangeTitle(this.updateTitleArea, this)); - this.message = $('.empty-message', { tabIndex: 0 }, localize('no open repo', "No source control providers registered.")); - const viewsRegistry = Registry.as(Extensions.ViewsRegistry); + + viewsRegistry.registerViewWelcomeContent(EmptyPane.ID, { + content: localize('no open repo', "No source control providers registered."), + when: 'default' + }); + + viewsRegistry.registerViews([new EmptyPaneDescriptor()], this.viewContainer); viewsRegistry.registerViews([new MainPaneDescriptor(this)], this.viewContainer); this._register(configurationService.onDidChangeConfiguration(e => { @@ -113,11 +153,7 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode create(parent: HTMLElement): void { super.create(parent); - - this.el = parent; - addClasses(parent, 'scm-viewlet', 'empty'); - append(parent, this.message); - + addClass(parent, 'scm-viewlet'); this._register(this.scmService.onDidAddRepository(this.onDidAddRepository, this)); this._register(this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this)); this.scmService.repositories.forEach(r => this.onDidAddRepository(r)); @@ -156,9 +192,7 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode } private onDidChangeRepositories(): void { - const repositoryCount = this.repositories.length; - toggleClass(this.el, 'empty', repositoryCount === 0); - this.repositoryCountKey.set(repositoryCount); + this.repositoryCountKey.set(this.repositories.length); } private onDidShowView(e: IAddedViewDescriptorRef[]): void { @@ -187,23 +221,19 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode } focus(): void { - if (this.repositoryCountKey.get()! === 0) { - this.message.focus(); - } else { - const repository = this.visibleRepositories[0]; + const repository = this.visibleRepositories[0]; - if (repository) { - const pane = this.panes - .filter(pane => pane instanceof RepositoryPane && pane.repository === repository)[0] as RepositoryPane | undefined; + if (repository) { + const pane = this.panes + .filter(pane => pane instanceof RepositoryPane && pane.repository === repository)[0] as RepositoryPane | undefined; - if (pane) { - pane.focus(); - } else { - super.focus(); - } + if (pane) { + pane.focus(); } else { super.focus(); } + } else { + super.focus(); } } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index 653cf67456be..1884a02ec443 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -26,7 +26,7 @@ import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileE import * as SearchConstants from 'vs/workbench/contrib/search/common/constants'; import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { OpenResultsInEditorAction, OpenSearchEditorAction, ReRunSearchEditorSearchAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; +import { OpenResultsInEditorAction, OpenSearchEditorAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; @@ -164,10 +164,6 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const registry = Registry.as(ActionExtensions.WorkbenchActions); const category = localize('search', "Search Editor"); -registry.registerWorkbenchAction( - SyncActionDescriptor.create(ReRunSearchEditorSearchAction, ReRunSearchEditorSearchAction.ID, ReRunSearchEditorSearchAction.LABEL), - 'Search Editor: Rerun search', category, ContextKeyExpr.and(SearchEditorConstants.InSearchEditor)); - registry.registerWorkbenchAction( SyncActionDescriptor.create(OpenResultsInEditorAction, OpenResultsInEditorAction.ID, OpenResultsInEditorAction.LABEL, { mac: { primary: KeyMod.CtrlCmd | KeyCode.Enter } }, diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index cabfbd9a6ca0..e07b696c2cf2 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -112,25 +112,6 @@ export class OpenResultsInEditorAction extends Action { } } - -export class ReRunSearchEditorSearchAction extends Action { - - static readonly ID = 'searchEditor.rerunSerach'; - static readonly LABEL = localize('search.rerunSearch', "Rerun Search in Editor"); - - constructor(id: string, label: string, - @IEditorService private readonly editorService: IEditorService) { - super(id, label); - } - - async run() { - const input = this.editorService.activeEditor; - if (input instanceof SearchEditorInput) { - await (this.editorService.activeControl as SearchEditor).runSearch(false, true); - } - } -} - const openNewSearchEditor = async (accessor: ServicesAccessor) => { const editorService = accessor.get(IEditorService); diff --git a/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css b/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css index ae431e80709b..ad6200c65617 100644 --- a/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css +++ b/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css @@ -13,3 +13,20 @@ position: absolute; pointer-events: none; } + +.timeline-tree-view .monaco-list .monaco-list-row .custom-view-tree-node-item .monaco-icon-label { + flex: 1; + text-overflow: ellipsis; + overflow: hidden; +} + +.timeline-tree-view .monaco-list .monaco-list-row .custom-view-tree-node-item .timeline-timestamp-container { + margin-left: 2px; + margin-right: 4px; + text-overflow: ellipsis; + overflow: hidden; +} + +.timeline-tree-view .monaco-list .monaco-list-row .custom-view-tree-node-item .timeline-timestamp-container .timeline-timestamp { + opacity: 0.5; +} diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 241ee3dabc05..1afbedc65637 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -8,11 +8,11 @@ import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IListVirtualDelegate, IIdentityProvider, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; -import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { ITreeNode, ITreeRenderer, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { TreeResourceNavigator, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -20,7 +20,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { TimelineItem, ITimelineService, TimelineChangeEvent, TimelineProvidersChangeEvent, TimelineRequest, TimelineItemWithSource } from 'vs/workbench/contrib/timeline/common/timeline'; +import { ITimelineService, TimelineChangeEvent, TimelineProvidersChangeEvent, TimelineRequest, TimelineItem } from 'vs/workbench/contrib/timeline/common/timeline'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { SideBySideEditor, toResource } from 'vs/workbench/common/editor'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -31,10 +31,20 @@ import { IProgressService } from 'vs/platform/progress/common/progress'; import { VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; import { debounce } from 'vs/base/common/decorators'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IActionViewItemProvider, ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction, ActionRunner } from 'vs/base/common/actions'; +import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { MenuItemAction, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { fromNow } from 'vs/base/common/date'; + +// TODO[ECA]: Localize all the strings type TreeElement = TimelineItem; -// TODO[ECA]: Localize all the strings +interface TimelineActionContext { + uri: URI | undefined; + item: TreeElement; +} export class TimelinePane extends ViewPane { static readonly ID = 'timeline'; @@ -44,10 +54,12 @@ export class TimelinePane extends ViewPane { private _messageElement!: HTMLDivElement; private _treeElement!: HTMLDivElement; private _tree!: WorkbenchObjectTree; + private _treeRenderer: TimelineTreeRenderer | undefined; + private _menus: TimelineMenus; private _visibilityDisposables: DisposableStore | undefined; // private _excludedSources: Set | undefined; - private _items: TimelineItemWithSource[] = []; + private _items: TimelineItem[] = []; private _loadingMessageTimer: any | undefined; private _pendingRequests = new Map(); private _uri: URI | undefined; @@ -67,7 +79,9 @@ export class TimelinePane extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...options, titleMenuId: MenuId.TimelineTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + + this._menus = this._register(this.instantiationService.createInstance(TimelineMenus, this.id)); const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); scopedContextKeyService.createKey('view', TimelinePane.ID); @@ -88,6 +102,7 @@ export class TimelinePane extends ViewPane { } this._uri = uri; + this._treeRenderer?.setUri(uri); this.loadTimeline(); } @@ -187,7 +202,7 @@ export class TimelinePane extends ViewPane { let request = this._pendingRequests.get(source); request?.tokenSource.dispose(true); - request = this.timelineService.getTimelineRequest(source, this._uri, new CancellationTokenSource())!; + request = this.timelineService.getTimeline(source, this._uri, {}, new CancellationTokenSource(), { cacheResults: true })!; this._pendingRequests.set(source, request); request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source)); @@ -199,7 +214,7 @@ export class TimelinePane extends ViewPane { private async handleRequest(request: TimelineRequest) { let items; try { - items = await this.progressService.withProgress({ location: VIEWLET_ID }, () => request.items); + items = await this.progressService.withProgress({ location: VIEWLET_ID }, () => request.result.then(r => r?.items ?? [])); } catch { } @@ -211,7 +226,7 @@ export class TimelinePane extends ViewPane { this.replaceItems(request.source, items); } - private replaceItems(source: string, items?: TimelineItemWithSource[]) { + private replaceItems(source: string, items?: TimelineItem[]) { const hasItems = this._items.length !== 0; if (items?.length) { @@ -291,17 +306,20 @@ export class TimelinePane extends ViewPane { // DOM.addClass(this._treeElement, 'show-file-icons'); container.appendChild(this._treeElement); - const renderer = this.instantiationService.createInstance(TimelineTreeRenderer); - this._tree = >this.instantiationService.createInstance(WorkbenchObjectTree, 'TimelinePane', this._treeElement, new TimelineListVirtualDelegate(), [renderer], { + this._treeRenderer = this.instantiationService.createInstance(TimelineTreeRenderer, this._menus); + this._tree = >this.instantiationService.createInstance(WorkbenchObjectTree, 'TimelinePane', + this._treeElement, new TimelineListVirtualDelegate(), [this._treeRenderer], { identityProvider: new TimelineIdentityProvider(), keyboardNavigationLabelProvider: new TimelineKeyboardNavigationLabelProvider(), overrideStyles: { - listBackground: this.getBackgroundColor() + listBackground: this.getBackgroundColor(), + } }); const customTreeNavigator = new TreeResourceNavigator(this._tree, { openOnFocus: false, openOnSelection: false }); this._register(customTreeNavigator); + this._register(this._tree.onContextMenu(e => this.onContextMenu(this._menus, e))); this._register( customTreeNavigator.onDidOpenResource(e => { if (!e.browserEvent) { @@ -316,36 +334,112 @@ export class TimelinePane extends ViewPane { }) ); } -} -export class TimelineElementTemplate { - static readonly id = 'TimelineElementTemplate'; + private onContextMenu(menus: TimelineMenus, treeEvent: ITreeContextMenuEvent): void { + const item = treeEvent.element; + if (item === null) { + return; + } + const event: UIEvent = treeEvent.browserEvent; - constructor( - readonly container: HTMLElement, - readonly iconLabel: IconLabel, - readonly icon: HTMLElement - ) { } -} + event.preventDefault(); + event.stopPropagation(); -export class TimelineIdentityProvider implements IIdentityProvider { - getId(item: TimelineItem): { toString(): string } { - return `${item.id}|${item.timestamp}`; + this._tree.setFocus([item]); + const actions = menus.getResourceContextActions(item); + if (!actions.length) { + return; + } + + this.contextMenuService.showContextMenu({ + getAnchor: () => treeEvent.anchor, + getActions: () => actions, + getActionViewItem: (action) => { + const keybinding = this.keybindingService.lookupKeybinding(action.id); + if (keybinding) { + return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); + } + return undefined; + }, + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + this._tree.domFocus(); + } + }, + getActionsContext: (): TimelineActionContext => ({ uri: this._uri, item: item }), + actionRunner: new TimelineActionRunner() + }); } } -export class TimelineKeyboardNavigationLabelProvider implements IKeyboardNavigationLabelProvider { - getKeyboardNavigationLabel(element: TimelineItem): { toString(): string } { +export class TimelineElementTemplate implements IDisposable { + static readonly id = 'TimelineElementTemplate'; + + readonly actionBar: ActionBar; + readonly icon: HTMLElement; + readonly iconLabel: IconLabel; + readonly timestamp: HTMLSpanElement; + + constructor( + readonly container: HTMLElement, + actionViewItemProvider: IActionViewItemProvider + ) { + DOM.addClass(container, 'custom-view-tree-node-item'); + this.icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon')); + + this.iconLabel = new IconLabel(container, { supportHighlights: true, supportCodicons: true }); + + const timestampContainer = DOM.append(this.iconLabel.element, DOM.$('.timeline-timestamp-container')); + this.timestamp = DOM.append(timestampContainer, DOM.$('span.timeline-timestamp')); + + const actionsContainer = DOM.append(this.iconLabel.element, DOM.$('.actions')); + this.actionBar = new ActionBar(actionsContainer, { actionViewItemProvider: actionViewItemProvider }); + } + + dispose() { + this.iconLabel.dispose(); + this.actionBar.dispose(); + } + + reset() { + this.actionBar.clear(); + } +} + +export class TimelineIdentityProvider implements IIdentityProvider { + getId(item: TreeElement): { toString(): string } { + return item.handle; + } +} + +class TimelineActionRunner extends ActionRunner { + + runAction(action: IAction, { uri, item }: TimelineActionContext): Promise { + return action.run(...[ + { + $mid: 11, + handle: item.handle, + source: item.source, + uri: uri + }, + uri, + item.source, + ]); + } +} + +export class TimelineKeyboardNavigationLabelProvider implements IKeyboardNavigationLabelProvider { + getKeyboardNavigationLabel(element: TreeElement): { toString(): string } { return element.label; } } -export class TimelineListVirtualDelegate implements IListVirtualDelegate { - getHeight(_element: TimelineItem): number { +export class TimelineListVirtualDelegate implements IListVirtualDelegate { + getHeight(_element: TreeElement): number { return 22; } - getTemplateId(element: TimelineItem): string { + getTemplateId(element: TreeElement): string { return TimelineElementTemplate.id; } } @@ -353,14 +447,25 @@ export class TimelineListVirtualDelegate implements IListVirtualDelegate { readonly templateId: string = TimelineElementTemplate.id; - constructor(@IThemeService private _themeService: IThemeService) { } + private _actionViewItemProvider: IActionViewItemProvider; + + constructor( + private readonly _menus: TimelineMenus, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IThemeService private _themeService: IThemeService + ) { + this._actionViewItemProvider = (action: IAction) => action instanceof MenuItemAction + ? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action) + : undefined; + } + + private _uri: URI | undefined; + setUri(uri: URI | undefined) { + this._uri = uri; + } renderTemplate(container: HTMLElement): TimelineElementTemplate { - DOM.addClass(container, 'custom-view-tree-node-item'); - const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon')); - - const iconLabel = new IconLabel(container, { supportHighlights: true, supportCodicons: true }); - return new TimelineElementTemplate(container, iconLabel, icon); + return new TimelineElementTemplate(container, this._actionViewItemProvider); } renderElement( @@ -369,30 +474,74 @@ class TimelineTreeRenderer implements ITreeRenderer /^inline/.test(g)); + + menu.dispose(); + contextKeyService.dispose(); + + return result; + } +} diff --git a/src/vs/workbench/contrib/timeline/common/timeline.ts b/src/vs/workbench/contrib/timeline/common/timeline.ts index 65fe341ffc08..7ffca6086461 100644 --- a/src/vs/workbench/contrib/timeline/common/timeline.ts +++ b/src/vs/workbench/contrib/timeline/common/timeline.ts @@ -16,9 +16,11 @@ export function toKey(extension: ExtensionIdentifier | string, source: string) { } export interface TimelineItem { + handle: string; + source: string; + timestamp: number; label: string; - id?: string; icon?: URI, iconDark?: URI, themeIcon?: { id: string }, @@ -28,19 +30,29 @@ export interface TimelineItem { contextValue?: string; } -export interface TimelineItemWithSource extends TimelineItem { - source: string; -} - export interface TimelineChangeEvent { id: string; uri?: URI; } +export interface TimelineCursor { + cursor?: any; + before?: boolean; + limit?: number; +} + +export interface Timeline { + source: string; + items: TimelineItem[]; + + cursor?: any; + more?: boolean; +} + export interface TimelineProvider extends TimelineProviderDescriptor, IDisposable { onDidChange?: Event; - provideTimeline(uri: URI, token: CancellationToken): Promise; + provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }): Promise; } export interface TimelineProviderDescriptor { @@ -55,7 +67,7 @@ export interface TimelineProvidersChangeEvent { } export interface TimelineRequest { - readonly items: Promise; + readonly result: Promise; readonly source: string; readonly tokenSource: CancellationTokenSource; readonly uri: URI; @@ -72,9 +84,7 @@ export interface ITimelineService { getSources(): string[]; - getTimeline(uri: URI, token: CancellationToken): Promise; - - getTimelineRequest(id: string, uri: URI, tokenSource: CancellationTokenSource): TimelineRequest | undefined; + getTimeline(id: string, uri: URI, cursor: TimelineCursor, tokenSource: CancellationTokenSource, options?: { cacheResults?: boolean }): TimelineRequest | undefined; } const TIMELINE_SERVICE_ID = 'timeline'; diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index eda524418318..270381062722 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; // import { basename } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; -import { ITimelineService, TimelineProvider, TimelineItem, TimelineChangeEvent, TimelineProvidersChangeEvent } from './timeline'; +import { ITimelineService, TimelineChangeEvent, TimelineCursor, TimelineProvidersChangeEvent, TimelineProvider } from './timeline'; export class TimelineService implements ITimelineService { _serviceBrand: undefined; @@ -81,42 +81,7 @@ export class TimelineService implements ITimelineService { return [...this._providers.keys()]; } - async getTimeline(uri: URI, token: CancellationToken, predicate?: (provider: TimelineProvider) => boolean) { - this.logService.trace(`TimelineService#getTimeline(${uri.toString(true)})`); - - const requests: Promise<[string, TimelineItem[]]>[] = []; - - for (const provider of this._providers.values()) { - if (typeof provider.scheme === 'string') { - if (provider.scheme !== '*' && provider.scheme !== uri.scheme) { - continue; - } - } else if (!provider.scheme.includes(uri.scheme)) { - continue; - } - if (!(predicate?.(provider) ?? true)) { - continue; - } - - requests.push(provider.provideTimeline(uri, token).then(p => [provider.id, p])); - } - - const timelines = await Promise.all(requests); - - const timeline = []; - for (const [source, items] of timelines) { - if (items.length === 0) { - continue; - } - - timeline.push(...items.map(item => ({ ...item, source: source }))); - } - - timeline.sort((a, b) => b.timestamp - a.timestamp); - return timeline; - } - - getTimelineRequest(id: string, uri: URI, tokenSource: CancellationTokenSource) { + getTimeline(id: string, uri: URI, cursor: TimelineCursor, tokenSource: CancellationTokenSource, options?: { cacheResults?: boolean }) { this.logService.trace(`TimelineService#getTimeline(${id}): uri=${uri.toString(true)}`); const provider = this._providers.get(id); @@ -133,12 +98,16 @@ export class TimelineService implements ITimelineService { } return { - items: provider.provideTimeline(uri, tokenSource.token) - .then(items => { - items = items.map(item => ({ ...item, source: provider.id })); - items.sort((a, b) => (b.timestamp - a.timestamp) || b.source.localeCompare(a.source, undefined, { numeric: true, sensitivity: 'base' })); + result: provider.provideTimeline(uri, cursor, tokenSource.token, options) + .then(result => { + if (result === undefined) { + return undefined; + } - return items; + result.items = result.items.map(item => ({ ...item, source: provider.id })); + result.items.sort((a, b) => (b.timestamp - a.timestamp) || b.source.localeCompare(a.source, undefined, { numeric: true, sensitivity: 'base' })); + + return result; }), source: provider.id, tokenSource: tokenSource, diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index 91dc0c6fcafd..786014463b64 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -127,39 +127,41 @@ export class ProductContribution implements IWorkbenchContribution { @IHostService hostService: IHostService, @IProductService productService: IProductService ) { - if (!hostService.hasFocus) { - return; - } + hostService.hadLastFocus().then(hadLastFocus => { + if (!hadLastFocus) { + return; + } - const lastVersion = storageService.get(ProductContribution.KEY, StorageScope.GLOBAL, ''); - const shouldShowReleaseNotes = configurationService.getValue('update.showReleaseNotes'); + const lastVersion = storageService.get(ProductContribution.KEY, StorageScope.GLOBAL, ''); + const shouldShowReleaseNotes = configurationService.getValue('update.showReleaseNotes'); - // was there an update? if so, open release notes - const releaseNotesUrl = productService.releaseNotesUrl; - if (shouldShowReleaseNotes && !environmentService.args['skip-release-notes'] && releaseNotesUrl && lastVersion && productService.version !== lastVersion) { - showReleaseNotes(instantiationService, productService.version) - .then(undefined, () => { - notificationService.prompt( - severity.Info, - nls.localize('read the release notes', "Welcome to {0} v{1}! Would you like to read the Release Notes?", productService.nameLong, productService.version), - [{ - label: nls.localize('releaseNotes', "Release Notes"), - run: () => { - const uri = URI.parse(releaseNotesUrl); - openerService.open(uri); - } - }], - { sticky: true } - ); - }); - } + // was there an update? if so, open release notes + const releaseNotesUrl = productService.releaseNotesUrl; + if (shouldShowReleaseNotes && !environmentService.args['skip-release-notes'] && releaseNotesUrl && lastVersion && productService.version !== lastVersion) { + showReleaseNotes(instantiationService, productService.version) + .then(undefined, () => { + notificationService.prompt( + severity.Info, + nls.localize('read the release notes', "Welcome to {0} v{1}! Would you like to read the Release Notes?", productService.nameLong, productService.version), + [{ + label: nls.localize('releaseNotes', "Release Notes"), + run: () => { + const uri = URI.parse(releaseNotesUrl); + openerService.open(uri); + } + }], + { sticky: true } + ); + }); + } - // should we show the new license? - if (productService.licenseUrl && lastVersion && semver.satisfies(lastVersion, '<1.0.0') && semver.satisfies(productService.version, '>=1.0.0')) { - notificationService.info(nls.localize('licenseChanged', "Our license terms have changed, please click [here]({0}) to go through them.", productService.licenseUrl)); - } + // should we show the new license? + if (productService.licenseUrl && lastVersion && semver.satisfies(lastVersion, '<1.0.0') && semver.satisfies(productService.version, '>=1.0.0')) { + notificationService.info(nls.localize('licenseChanged', "Our license terms have changed, please click [here]({0}) to go through them.", productService.licenseUrl)); + } - storageService.store(ProductContribution.KEY, productService.version, StorageScope.GLOBAL); + storageService.store(ProductContribution.KEY, productService.version, StorageScope.GLOBAL); + }); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 5e756cd3704a..6af68ea3febd 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -147,13 +147,13 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } if (sessions.length === 0) { - this.setActiveAccount(undefined); + await this.setActiveAccount(undefined); return; } if (sessions.length === 1) { this.logAuthenticatedEvent(sessions[0]); - this.setActiveAccount(sessions[0]); + await this.setActiveAccount(sessions[0]); return; } @@ -167,7 +167,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (selectedAccount) { const selected = sessions.filter(account => selectedAccount.id === account.id)[0]; this.logAuthenticatedEvent(selected); - this.setActiveAccount(selected); + await this.setActiveAccount(selected); } } @@ -565,7 +565,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async signIn(): Promise { try { - this.setActiveAccount(await this.authenticationService.login(this.userDataSyncStore!.authenticationProviderId, ['https://management.core.windows.net/.default', 'offline_access'])); + await this.setActiveAccount(await this.authenticationService.login(this.userDataSyncStore!.authenticationProviderId, ['https://management.core.windows.net/.default', 'offline_access'])); } catch (e) { this.notificationService.error(e); throw e; @@ -575,7 +575,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async signOut(): Promise { if (this.activeAccount) { await this.authenticationService.logout(this.userDataSyncStore!.authenticationProviderId, this.activeAccount.id); - this.setActiveAccount(undefined); + await this.setActiveAccount(undefined); } } diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 5cf9df18ef8a..d8f0b88ebc3d 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -286,4 +286,10 @@ export abstract class BaseWebview extends Disposable { this.element.style.pointerEvents = ''; } } + + public selectAll() { + if (this.element) { + this._send('execCommand', 'selectAll'); + } + } } diff --git a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index 9848ce0cb706..fe444e8070df 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -187,6 +187,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewEd showFind(): void { this.withWebview(webview => webview.showFind()); } hideFind(): void { this.withWebview(webview => webview.hideFind()); } runFindAction(previous: boolean): void { this.withWebview(webview => webview.runFindAction(previous)); } + selectAll(): void { this.withWebview(webview => webview.selectAll()); } public getInnerWebview() { return this._webview.value; diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 63c9af47e20c..aa318da9d65b 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -232,23 +232,23 @@ * @param {MouseEvent} event */ const handleAuxClick = - (event) => { - // Prevent middle clicks opening a broken link in the browser - if (!event.view || !event.view.document) { - return; - } - - if (event.button === 1) { - let node = /** @type {any} */ (event.target); - while (node) { - if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) { - event.preventDefault(); - break; - } - node = node.parentNode; + (event) => { + // Prevent middle clicks opening a broken link in the browser + if (!event.view || !event.view.document) { + return; } - } - }; + + if (event.button === 1) { + let node = /** @type {any} */ (event.target); + while (node) { + if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) { + event.preventDefault(); + break; + } + node = node.parentNode; + } + } + }; /** * @param {KeyboardEvent} e @@ -449,6 +449,10 @@ }, 0); }); + /** + * @param {Document} contentDocument + * @param {Window} contentWindow + */ const onLoad = (contentDocument, contentWindow) => { if (contentDocument && contentDocument.body) { // Workaround for https://github.com/Microsoft/vscode/issues/12865 @@ -492,10 +496,12 @@ }, 200); newFrame.contentWindow.addEventListener('load', function (e) { + const contentDocument = /** @type {Document} */ (e.target); + if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = undefined; - onLoad(e.target, this); + onLoad(contentDocument, this); } }); @@ -539,6 +545,13 @@ initData.initialScrollProgress = progress; }); + host.onMessage('execCommand', (_event, data) => { + const target = getActiveFrame(); + if (!target) { + return; + } + target.contentDocument.execCommand(data); + }); trackFocus({ onFocus: () => host.postMessage('did-focus'), diff --git a/src/vs/workbench/contrib/webview/browser/webview.contribution.ts b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts index c1ce99713e4f..b8240e1bc17a 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.contribution.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts @@ -14,7 +14,7 @@ import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/wor import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { webviewDeveloperCategory } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory'; -import { HideWebViewEditorFindCommand, ReloadWebviewAction, ShowWebViewEditorFindWidgetAction, WebViewEditorFindNextCommand, WebViewEditorFindPreviousCommand } from '../browser/webviewCommands'; +import { HideWebViewEditorFindCommand, ReloadWebviewAction, ShowWebViewEditorFindWidgetAction, WebViewEditorFindNextCommand, WebViewEditorFindPreviousCommand, SelectAllWebviewEditorCommand } from '../browser/webviewCommands'; import { WebviewEditor } from './webviewEditor'; import { WebviewInput } from './webviewEditorInput'; import { IWebviewWorkbenchService, WebviewEditorService } from './webviewWorkbenchService'; @@ -50,6 +50,11 @@ registerAction2(class extends WebViewEditorFindPreviousCommand { constructor() { super(webviewActiveContextKeyExpr); } }); +registerAction2(class extends SelectAllWebviewEditorCommand { + constructor() { super(webviewActiveContextKeyExpr); } +}); + + const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); actionRegistry.registerWorkbenchAction( SyncActionDescriptor.create(ReloadWebviewAction, ReloadWebviewAction.ID, ReloadWebviewAction.LABEL), diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 784bef65db4d..b65f28416f2b 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -93,6 +93,8 @@ export interface Webview extends IDisposable { hideFind(): void; runFindAction(previous: boolean): void; + selectAll(): void; + windowDidDragStart(): void; windowDidDragEnd(): void; } diff --git a/src/vs/workbench/contrib/webview/browser/webviewCommands.ts b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts index 9845fb414d91..5f870d1d15e5 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewCommands.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts @@ -13,6 +13,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; export class ShowWebViewEditorFindWidgetAction extends Action2 { public static readonly ID = 'editor.action.webvieweditor.showFind'; @@ -97,6 +98,29 @@ export class WebViewEditorFindPreviousCommand extends Action2 { getActiveWebviewEditor(accessor)?.find(true); } } + +export class SelectAllWebviewEditorCommand extends Action2 { + public static readonly ID = 'editor.action.webvieweditor.selectAll'; + public static readonly LABEL = nls.localize('editor.action.webvieweditor.selectAll', 'Select all'); + + constructor(contextKeyExpr: ContextKeyExpr) { + const precondition = ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)); + super({ + id: SelectAllWebviewEditorCommand.ID, + title: SelectAllWebviewEditorCommand.LABEL, + keybinding: { + when: precondition, + primary: KeyMod.CtrlCmd | KeyCode.KEY_A, + weight: KeybindingWeight.EditorContrib + } + }); + } + + public run(accessor: ServicesAccessor, args: any): void { + getActiveWebviewEditor(accessor)?.selectAll(); + } +} + export class ReloadWebviewAction extends Action { static readonly ID = 'workbench.action.webview.reloadWebviewAction'; static readonly LABEL = nls.localize('refreshWebviewLabel', "Reload Webviews"); diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts index a0e741c29170..97eb2c50bd0d 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts @@ -15,7 +15,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; -import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, Webview, WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -71,31 +71,33 @@ export class WebviewEditor extends BaseEditor { } public showFind() { - this.withWebview(webview => { - webview.showFind(); + if (this.webview) { + this.webview.showFind(); this._findWidgetVisible.set(true); - }); + } } public hideFind() { this._findWidgetVisible.reset(); - this.withWebview(webview => webview.hideFind()); + this.webview?.hideFind(); } public find(previous: boolean) { - this.withWebview(webview => { - webview.runFindAction(previous); - }); + this.webview?.runFindAction(previous); + } + + public selectAll() { + this.webview?.selectAll(); } public reload() { - this.withWebview(webview => webview.reload()); + this.webview?.reload(); } public layout(dimension: DOM.Dimension): void { this._dimension = dimension; - if (this.input && this.input instanceof WebviewInput) { - this.synchronizeWebviewContainerDimensions(this.input.webview, dimension); + if (this.webview) { + this.synchronizeWebviewContainerDimensions(this.webview, dimension); } } @@ -109,22 +111,19 @@ export class WebviewEditor extends BaseEditor { } }); } - this.withWebview(webview => webview.focus()); + this.webview?.focus(); } - public withWebview(f: (element: Webview) => void): void { - if (this.input && this.input instanceof WebviewInput) { - f(this.input.webview); - } + public get webview(): WebviewEditorOverlay | undefined { + return this.input instanceof WebviewInput ? this.input.webview : undefined; } protected setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - if (this.input instanceof WebviewInput) { - const webview = this.input.webview; + if (this.input instanceof WebviewInput && this.webview) { if (visible) { - webview.claim(this); + this.webview.claim(this); } else { - webview.release(this); + this.webview.release(this); } this.claimWebview(this.input); } @@ -132,8 +131,8 @@ export class WebviewEditor extends BaseEditor { } public clearInput() { - if (this.input && this.input instanceof WebviewInput) { - this.input.webview.release(this); + if (this.webview) { + this.webview.release(this); this._webviewVisibleDisposables.clear(); } @@ -145,8 +144,8 @@ export class WebviewEditor extends BaseEditor { return; } - if (this.input && this.input instanceof WebviewInput) { - this.input.webview.release(this); + if (this.webview) { + this.webview.release(this); } await super.setInput(input, options, token); @@ -189,15 +188,11 @@ export class WebviewEditor extends BaseEditor { } this._webviewVisibleDisposables.add(DOM.addDisposableListener(window, DOM.EventType.DRAG_START, () => { - if (this.input instanceof WebviewInput) { - this.input.webview.windowDidDragStart(); - } + this.webview?.windowDidDragStart(); })); const onDragEnd = () => { - if (this.input instanceof WebviewInput) { - this.input.webview.windowDidDragEnd(); - } + this.webview?.windowDidDragEnd(); }; this._webviewVisibleDisposables.add(DOM.addDisposableListener(window, DOM.EventType.DRAG_END, onDragEnd)); this._webviewVisibleDisposables.add(DOM.addDisposableListener(window, DOM.EventType.MOUSE_MOVE, currentEvent => { diff --git a/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts b/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts index 538e36abc333..b0471a3d0a13 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts @@ -26,8 +26,6 @@ actionRegistry.registerWorkbenchAction( function registerWebViewCommands(editorId: string): void { const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', editorId), ContextKeyExpr.not('editorFocus') /* https://github.com/Microsoft/vscode/issues/58668 */)!; - registerAction2(class extends webviewCommands.SelectAllWebviewEditorCommand { constructor() { super(contextKeyExpr); } }); - // These commands are only needed on MacOS where we have to disable the menu bar commands if (isMacintosh) { registerAction2(class extends webviewCommands.CopyWebviewEditorCommand { constructor() { super(contextKeyExpr); } }); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts index 611622ed72b1..ebbe40c7f428 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts @@ -38,28 +38,6 @@ export class OpenWebviewDeveloperToolsAction extends Action { } } -export class SelectAllWebviewEditorCommand extends Action2 { - public static readonly ID = 'editor.action.webvieweditor.selectAll'; - public static readonly LABEL = nls.localize('editor.action.webvieweditor.selectAll', 'Select all'); - - constructor(contextKeyExpr: ContextKeyExpr) { - const precondition = ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)); - super({ - id: SelectAllWebviewEditorCommand.ID, - title: SelectAllWebviewEditorCommand.LABEL, - keybinding: { - when: precondition, - primary: KeyMod.CtrlCmd | KeyCode.KEY_A, - weight: KeybindingWeight.EditorContrib - } - }); - } - - public run(accessor: ServicesAccessor, args: any): void { - withActiveWebviewBasedWebview(accessor, webview => webview.selectAll()); - } -} - export class CopyWebviewEditorCommand extends Action2 { public static readonly ID = 'editor.action.webvieweditor.copy'; public static readonly LABEL = nls.localize('editor.action.webvieweditor.copy', "Copy2"); @@ -77,7 +55,7 @@ export class CopyWebviewEditorCommand extends Action2 { } public run(accessor: ServicesAccessor): void { - withActiveWebviewBasedWebview(accessor, webview => webview.copy()); + getActiveWebviewBasedWebview(accessor)?.copy(); } } @@ -98,7 +76,7 @@ export class PasteWebviewEditorCommand extends Action2 { } public run(accessor: ServicesAccessor): void { - withActiveWebviewBasedWebview(accessor, webview => webview.paste()); + getActiveWebviewBasedWebview(accessor)?.paste(); } } @@ -119,7 +97,7 @@ export class CutWebviewEditorCommand extends Action2 { } public run(accessor: ServicesAccessor): void { - withActiveWebviewBasedWebview(accessor, webview => webview.cut()); + getActiveWebviewBasedWebview(accessor)?.cut(); } } @@ -140,7 +118,7 @@ export class UndoWebviewEditorCommand extends Action2 { } public run(accessor: ServicesAccessor, args: any): void { - withActiveWebviewBasedWebview(accessor, webview => webview.undo()); + getActiveWebviewBasedWebview(accessor)?.undo(); } } @@ -163,22 +141,24 @@ export class RedoWebviewEditorCommand extends Action2 { } public run(accessor: ServicesAccessor, args: any): void { - withActiveWebviewBasedWebview(accessor, webview => webview.redo()); + getActiveWebviewBasedWebview(accessor)?.redo(); } } -function withActiveWebviewBasedWebview(accessor: ServicesAccessor, f: (webview: ElectronWebviewBasedWebview) => void): void { - const webViewEditor = getActiveWebviewEditor(accessor); - if (webViewEditor) { - webViewEditor.withWebview(webview => { - if (webview instanceof ElectronWebviewBasedWebview) { - f(webview); - } else if ((webview as WebviewEditorOverlay).getInnerWebview) { - const innerWebview = (webview as WebviewEditorOverlay).getInnerWebview(); - if (innerWebview instanceof ElectronWebviewBasedWebview) { - f(innerWebview); - } - } - }); +function getActiveWebviewBasedWebview(accessor: ServicesAccessor): ElectronWebviewBasedWebview | undefined { + const webview = getActiveWebviewEditor(accessor)?.webview; + if (!webview) { + return undefined; } + + if (webview instanceof ElectronWebviewBasedWebview) { + return webview; + } else if ((webview as WebviewEditorOverlay).getInnerWebview) { + const innerWebview = (webview as WebviewEditorOverlay).getInnerWebview(); + if (innerWebview instanceof ElectronWebviewBasedWebview) { + return innerWebview; + } + } + + return undefined; } diff --git a/src/vs/workbench/contrib/welcome/common/viewsWelcome.contribution.ts b/src/vs/workbench/contrib/welcome/common/viewsWelcome.contribution.ts new file mode 100644 index 000000000000..9153e3c1a4d8 --- /dev/null +++ b/src/vs/workbench/contrib/welcome/common/viewsWelcome.contribution.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { ViewsWelcomeContribution } from 'vs/workbench/contrib/welcome/common/viewsWelcomeContribution'; +import { ViewsWelcomeExtensionPoint, viewsWelcomeExtensionPointDescriptor } from 'vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; + +const extensionPoint = ExtensionsRegistry.registerExtensionPoint(viewsWelcomeExtensionPointDescriptor); + +class WorkbenchConfigurationContribution { + constructor( + @IInstantiationService instantiationService: IInstantiationService, + ) { + instantiationService.createInstance(ViewsWelcomeContribution, extensionPoint); + } +} + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(WorkbenchConfigurationContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts b/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts new file mode 100644 index 000000000000..51fb3194e959 --- /dev/null +++ b/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { ViewsWelcomeExtensionPoint, ViewWelcome, viewsWelcomeExtensionPointDescriptor } from './viewsWelcomeExtensionPoint'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as ViewContainerExtensions, IViewsRegistry } from 'vs/workbench/common/views'; +import { localize } from 'vs/nls'; + +const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); + +export class ViewsWelcomeContribution extends Disposable implements IWorkbenchContribution { + + private viewWelcomeContents = new Map(); + + constructor(extensionPoint: IExtensionPoint) { + super(); + + extensionPoint.setHandler((_, { added, removed }) => { + for (const contribution of removed) { + // Proposed API check + if (!contribution.description.enableProposedApi) { + continue; + } + + for (const welcome of contribution.value) { + const disposable = this.viewWelcomeContents.get(welcome); + + if (disposable) { + disposable.dispose(); + } + } + } + + for (const contribution of added) { + // Proposed API check + if (!contribution.description.enableProposedApi) { + contribution.collector.error(localize('proposedAPI.invalid', "The '{0}' contribution is a proposed API and is only available when running out of dev or with the following command line switch: --enable-proposed-api {1}", viewsWelcomeExtensionPointDescriptor.extensionPoint, contribution.description.identifier.value)); + continue; + } + + for (const welcome of contribution.value) { + const disposable = viewsRegistry.registerViewWelcomeContent(welcome.view, { + content: welcome.contents, + when: ContextKeyExpr.deserialize(welcome.when) + }); + + this.viewWelcomeContents.set(welcome, disposable); + } + } + }); + } +} diff --git a/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts b/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts new file mode 100644 index 000000000000..28a5bc02dfad --- /dev/null +++ b/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; + +export enum ViewsWelcomeExtensionPointFields { + view = 'view', + contents = 'contents', + when = 'when', +} + +export interface ViewWelcome { + readonly [ViewsWelcomeExtensionPointFields.view]: string; + readonly [ViewsWelcomeExtensionPointFields.contents]: string; + readonly [ViewsWelcomeExtensionPointFields.when]: string; +} + +export type ViewsWelcomeExtensionPoint = ViewWelcome[]; + +const viewsWelcomeExtensionPointSchema = Object.freeze({ + type: 'array', + description: nls.localize('contributes.viewsWelcome', "Contributed views welcome content."), + items: { + type: 'object', + description: nls.localize('contributes.viewsWelcome.view', "Contributed welcome content for a specific view."), + required: [ + ViewsWelcomeExtensionPointFields.view, + ViewsWelcomeExtensionPointFields.contents + ], + properties: { + [ViewsWelcomeExtensionPointFields.view]: { + type: 'string', + description: nls.localize('contributes.viewsWelcome.view.view', "View identifier for this welcome content."), + }, + [ViewsWelcomeExtensionPointFields.contents]: { + type: 'string', + description: nls.localize('contributes.viewsWelcome.view.contents', "Welcome content."), + }, + [ViewsWelcomeExtensionPointFields.when]: { + type: 'string', + description: nls.localize('contributes.viewsWelcome.view.when', "When clause for this welcome content."), + }, + } + } +}); + +export const viewsWelcomeExtensionPointDescriptor = { + extensionPoint: 'viewsWelcome', + jsonSchema: viewsWelcomeExtensionPointSchema +}; diff --git a/src/vs/workbench/electron-browser/desktop.contribution.ts b/src/vs/workbench/electron-browser/desktop.contribution.ts index 61e2e2e58850..f1d8f9af2533 100644 --- a/src/vs/workbench/electron-browser/desktop.contribution.ts +++ b/src/vs/workbench/electron-browser/desktop.contribution.ts @@ -22,6 +22,7 @@ import { NoEditorsVisibleContext, SingleEditorGroupsContext } from 'vs/workbench import { IElectronService } from 'vs/platform/electron/node/electron'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import product from 'vs/platform/product/common/product'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; // Actions (function registerActions(): void { @@ -321,8 +322,7 @@ import product from 'vs/platform/product/common/product'; (function registerJSONSchemas(): void { const argvDefinitionFileSchemaId = 'vscode://schemas/argv'; const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); - - jsonRegistry.registerSchema(argvDefinitionFileSchemaId, { + const schema: IJSONSchema = { id: argvDefinitionFileSchemaId, allowComments: true, allowTrailingCommas: true, @@ -343,5 +343,13 @@ import product from 'vs/platform/product/common/product'; description: nls.localize('argv.disableColorCorrectRendering', 'Resolves issues around color profile selection. ONLY change this option if you encounter graphic issues.') } } - }); + }; + if (isLinux) { + schema.properties!['force-renderer-accessibility'] = { + type: 'boolean', + description: nls.localize('argv.force-renderer-accessibility', 'Forces the renderer to be accessible. ONLY change this if you are using a screen reader on Linux. On other platforms the renderer will automatically be accessible. This flag is automatically set if you have editor.accessibilitySupport: on.'), + }; + } + + jsonRegistry.registerSchema(argvDefinitionFileSchemaId, schema); })(); diff --git a/src/vs/workbench/services/accessibility/node/accessibilityService.ts b/src/vs/workbench/services/accessibility/node/accessibilityService.ts index faedc3fd3186..a2911ba04a57 100644 --- a/src/vs/workbench/services/accessibility/node/accessibilityService.ts +++ b/src/vs/workbench/services/accessibility/node/accessibilityService.ts @@ -4,13 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; -import { isWindows } from 'vs/base/common/platform'; +import { isWindows, isLinux } from 'vs/base/common/platform'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Registry } from 'vs/platform/registry/common/platform'; import { AccessibilityService } from 'vs/platform/accessibility/common/accessibilityService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; interface AccessibilityMetrics { enabled: boolean; @@ -65,3 +70,22 @@ export class NodeAccessibilityService extends AccessibilityService implements IA } registerSingleton(IAccessibilityService, NodeAccessibilityService, true); + +// On linux we do not automatically detect that a screen reader is detected, thus we have to implicitly notify the renderer to enable accessibility when user configures it in settings +class LinuxAccessibilityContribution implements IWorkbenchContribution { + constructor( + @IJSONEditingService jsonEditingService: IJSONEditingService, + @IAccessibilityService accessibilityService: AccessibilityService, + @IEnvironmentService environmentService: IEnvironmentService + ) { + accessibilityService.onDidChangeScreenReaderOptimized(async () => { + if (accessibilityService.isScreenReaderOptimized()) { + await jsonEditingService.write(environmentService.argvResource, [{ key: 'force-renderer-accessibility', value: true }], true); + } + }); + } +} + +if (isLinux) { + Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LinuxAccessibilityContribution, LifecyclePhase.Ready); +} diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index 41423b188fa6..01d63f94a76a 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -13,7 +13,7 @@ import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementSer import { IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationHandle, INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IURLHandler, IURLService, IOpenURLOptions } from 'vs/platform/url/common/url'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -26,6 +26,7 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; const FIVE_MINUTES = 5 * 60 * 1000; const THIRTY_SECONDS = 30 * 1000; @@ -100,7 +101,8 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { @IHostService private readonly hostService: IHostService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IStorageService private readonly storageService: IStorageService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IProgressService private readonly progressService: IProgressService ) { this.storage = new ConfirmedExtensionIdStorage(storageService); @@ -273,32 +275,20 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { return; } - let notificationHandle: INotificationHandle | null = this.notificationService.notify({ severity: Severity.Info, message: localize('Installing', "Installing Extension '{0}'...", galleryExtension.displayName || galleryExtension.name) }); - notificationHandle.progress.infinite(); - notificationHandle.onDidClose(() => notificationHandle = null); - try { - await this.extensionManagementService.installFromGallery(galleryExtension); - const reloadMessage = localize('reload', "Would you like to reload the window and open the URL '{0}'?", uri.toString()); - const reloadActionLabel = localize('Reload', "Reload Window and Open"); + await this.progressService.withProgress({ + location: ProgressLocation.Notification, + title: localize('Installing', "Installing Extension '{0}'...", galleryExtension.displayName || galleryExtension.name) + }, () => this.extensionManagementService.installFromGallery(galleryExtension)); - if (notificationHandle) { - notificationHandle.progress.done(); - notificationHandle.updateMessage(reloadMessage); - notificationHandle.updateActions({ - primary: [new Action('reloadWindow', reloadActionLabel, undefined, true, () => this.reloadAndHandle(uri))] - }); - } else { - this.notificationService.prompt(Severity.Info, reloadMessage, [{ label: reloadActionLabel, run: () => this.reloadAndHandle(uri) }], { sticky: true }); - } - } catch (e) { - if (notificationHandle) { - notificationHandle.progress.done(); - notificationHandle.updateSeverity(Severity.Error); - notificationHandle.updateMessage(e); - } else { - this.notificationService.error(e); - } + this.notificationService.prompt( + Severity.Info, + localize('reload', "Would you like to reload the window and open the URL '{0}'?", uri.toString()), + [{ label: localize('Reload', "Reload Window and Open"), run: () => this.reloadAndHandle(uri) }], + { sticky: true } + ); + } catch (error) { + this.notificationService.error(error); } } } diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 7c0b27ba4905..85de4203e540 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -95,6 +95,10 @@ export class BrowserHostService extends Disposable implements IHostService { return document.hasFocus(); } + async hadLastFocus(): Promise { + return true; + } + async focus(): Promise { window.focus(); } diff --git a/src/vs/workbench/services/host/browser/host.ts b/src/vs/workbench/services/host/browser/host.ts index f97c8048de29..ad10de5ba655 100644 --- a/src/vs/workbench/services/host/browser/host.ts +++ b/src/vs/workbench/services/host/browser/host.ts @@ -25,6 +25,11 @@ export interface IHostService { */ readonly hasFocus: boolean; + /** + * Find out if the window had the last focus. + */ + hadLastFocus(): Promise; + /** * Attempt to bring the window to the foreground and focus it. */ diff --git a/src/vs/workbench/services/host/electron-browser/desktopHostService.ts b/src/vs/workbench/services/host/electron-browser/desktopHostService.ts index 6fd4fa2404ea..b009d228026a 100644 --- a/src/vs/workbench/services/host/electron-browser/desktopHostService.ts +++ b/src/vs/workbench/services/host/electron-browser/desktopHostService.ts @@ -36,6 +36,16 @@ export class DesktopHostService extends Disposable implements IHostService { return document.hasFocus(); } + async hadLastFocus(): Promise { + const activeWindowId = await this.electronService.getActiveWindowId(); + + if (typeof activeWindowId === 'undefined') { + return false; + } + + return activeWindowId === this.electronEnvironmentService.windowId; + } + openWindow(options?: IOpenEmptyWindowOptions): Promise; openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; openWindow(arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise { diff --git a/src/vs/workbench/services/progress/browser/media/progressService.css b/src/vs/workbench/services/progress/browser/media/progressService.css index 6b73a2f3a4e2..a1e92a76439f 100644 --- a/src/vs/workbench/services/progress/browser/media/progressService.css +++ b/src/vs/workbench/services/progress/browser/media/progressService.css @@ -3,14 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.progress { - padding-left: 5px; -} - -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.progress .spinner-container { - padding-right: 5px; -} - .monaco-workbench .progress-badge > .badge-content::before { mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMiAyIDE0IDE0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDIgMiAxNCAxNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkgMTZjLTMuODYgMC03LTMuMTQtNy03czMuMTQtNyA3LTdjMy44NTkgMCA3IDMuMTQxIDcgN3MtMy4xNDEgNy03IDd6bTAtMTIuNmMtMy4wODggMC01LjYgMi41MTMtNS42IDUuNnMyLjUxMiA1LjYgNS42IDUuNiA1LjYtMi41MTIgNS42LTUuNi0yLjUxMi01LjYtNS42LTUuNnptMy44NiA3LjFsLTMuMTYtMS44OTZ2LTMuODA0aC0xLjR2NC41OTZsMy44NCAyLjMwNS43Mi0xLjIwMXoiLz48L3N2Zz4="); -webkit-mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMiAyIDE0IDE0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDIgMiAxNCAxNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkgMTZjLTMuODYgMC03LTMuMTQtNy03czMuMTQtNyA3LTdjMy44NTkgMCA3IDMuMTQxIDcgN3MtMy4xNDEgNy03IDd6bTAtMTIuNmMtMy4wODggMC01LjYgMi41MTMtNS42IDUuNnMyLjUxMiA1LjYgNS42IDUuNiA1LjYtMi41MTIgNS42LTUuNi0yLjUxMi01LjYtNS42LTUuNnptMy44NiA3LjFsLTMuMTYtMS44OTZ2LTMuODA0aC0xLjR2NC41OTZsMy44NCAyLjMwNS43Mi0xLjIwMXoiLz48L3N2Zz4="); diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 4a5831b56671..4ffbfbbd4c8f 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -12,9 +12,9 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { StatusbarAlignment, IStatusbarService } from 'vs/workbench/services/statusbar/common/statusbar'; import { timeout } from 'vs/base/common/async'; import { ProgressBadge, IActivityService } from 'vs/workbench/services/activity/common/activity'; -import { INotificationService, Severity, INotificationHandle, INotificationActions } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity, INotificationHandle } from 'vs/platform/notification/common/notification'; import { Action } from 'vs/base/common/actions'; -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { Dialog } from 'vs/base/browser/ui/dialog/dialog'; @@ -143,10 +143,72 @@ export class ProgressService extends Disposable implements IProgressService { } } - private withNotificationProgress

, R = unknown>(options: IProgressNotificationOptions, callback: (progress: IProgress<{ message?: string, increment?: number }>) => P, onDidCancel?: (choice?: number) => void): P { - const toDispose = new DisposableStore(); + private withNotificationProgress

, R = unknown>(options: IProgressNotificationOptions, callback: (progress: IProgress) => P, onDidCancel?: (choice?: number) => void): P { + + const progressStateModel = new class extends Disposable { + + private readonly _onDidReport = this._register(new Emitter()); + readonly onDidReport = this._onDidReport.event; + + private readonly _onDispose = this._register(new Emitter()); + readonly onDispose = this._onDispose.event; + + private _step: IProgressStep | undefined = undefined; + get step() { return this._step; } + + private _done = false; + get done() { return this._done; } + + readonly promise: P; + + constructor() { + super(); + + this.promise = callback(this); + + this.promise.finally(() => { + this.dispose(); + }); + } + + report(step: IProgressStep): void { + this._step = step; + + this._onDidReport.fire(step); + } + + cancel(choice?: number): void { + onDidCancel?.(choice); + + this.dispose(); + } + + dispose(): void { + this._done = true; + this._onDispose.fire(); + + super.dispose(); + } + }; + + const createWindowProgress = () => { + this.withWindowProgress({ + location: ProgressLocation.Window, + title: options.title + }, progress => { + if (progressStateModel.step) { + progress.report(progressStateModel.step); + } + + const disposable = progressStateModel.onDidReport(step => progress.report(step)); + Event.once(progressStateModel.onDispose)(() => disposable.dispose()); + + return progressStateModel.promise; + }); + }; const createNotification = (message: string, increment?: number): INotificationHandle => { + const notificationDisposables = new DisposableStore(); const primaryActions = options.primaryActions ? Array.from(options.primaryActions) : []; const secondaryActions = options.secondaryActions ? Array.from(options.secondaryActions) : []; @@ -158,16 +220,11 @@ export class ProgressService extends Disposable implements IProgressService { super(`progress.button.${button}`, button, undefined, true); } - run(): Promise { - if (typeof onDidCancel === 'function') { - onDidCancel(index); - } - - return Promise.resolve(undefined); + async run(): Promise { + progressStateModel.cancel(index); } }; - - toDispose.add(buttonAction); + notificationDisposables.add(buttonAction); primaryActions.push(buttonAction); }); @@ -179,31 +236,35 @@ export class ProgressService extends Disposable implements IProgressService { super('progress.cancel', localize('cancel', "Cancel"), undefined, true); } - run(): Promise { - if (typeof onDidCancel === 'function') { - onDidCancel(); - } - - return Promise.resolve(undefined); + async run(): Promise { + progressStateModel.cancel(); } }; - toDispose.add(cancelAction); + notificationDisposables.add(cancelAction); primaryActions.push(cancelAction); } - const actions: INotificationActions = { primary: primaryActions, secondary: secondaryActions }; const handle = this.notificationService.notify({ severity: Severity.Info, message, source: options.source, - actions + actions: { primary: primaryActions, secondary: secondaryActions } }); updateProgress(handle, increment); Event.once(handle.onDidClose)(() => { - toDispose.dispose(); + + // Switch to window based progress once the notification + // is being closed even though still running and not + // cancelled. + if (!progressStateModel.done) { + createWindowProgress(); + } + + // Clear disposables + notificationDisposables.dispose(); }); return handle; @@ -218,60 +279,54 @@ export class ProgressService extends Disposable implements IProgressService { } }; - let handle: INotificationHandle | undefined; - let handleSoon: any | undefined; - + let notificationHandle: INotificationHandle | undefined; + let notificationTimeout: any | undefined; let titleAndMessage: string | undefined; // hoisted to make sure a delayed notification shows the most recent message - const updateNotification = (message?: string, increment?: number): void => { + const updateNotification = (step?: IProgressStep): void => { // full message (inital or update) - if (message && options.title) { - titleAndMessage = `${options.title}: ${message}`; // always prefix with overall title if we have it (https://github.com/Microsoft/vscode/issues/50932) + if (step?.message && options.title) { + titleAndMessage = `${options.title}: ${step.message}`; // always prefix with overall title if we have it (https://github.com/Microsoft/vscode/issues/50932) } else { - titleAndMessage = options.title || message; + titleAndMessage = options.title || step?.message; } - if (!handle && titleAndMessage) { + if (!notificationHandle && titleAndMessage) { + // create notification now or after a delay if (typeof options.delay === 'number' && options.delay > 0) { - if (typeof handleSoon !== 'number') { - handleSoon = setTimeout(() => handle = createNotification(titleAndMessage!, increment), options.delay); + if (typeof notificationTimeout !== 'number') { + notificationTimeout = setTimeout(() => notificationHandle = createNotification(titleAndMessage!, step?.increment), options.delay); } } else { - handle = createNotification(titleAndMessage, increment); + notificationHandle = createNotification(titleAndMessage, step?.increment); } } - if (handle) { + if (notificationHandle) { if (titleAndMessage) { - handle.updateMessage(titleAndMessage); + notificationHandle.updateMessage(titleAndMessage); } - if (typeof increment === 'number') { - updateProgress(handle, increment); + + if (typeof step?.increment === 'number') { + updateProgress(notificationHandle, step.increment); } } }; // Show initially - updateNotification(); - - // Update based on progress - const promise = callback({ - report: progress => { - updateNotification(progress.message, progress.increment); - } - }); + updateNotification(progressStateModel.step); + const listener = progressStateModel.onDidReport(step => updateNotification(step)); + Event.once(progressStateModel.onDispose)(() => listener.dispose()); // Show progress for at least 800ms and then hide once done or canceled - Promise.all([timeout(800), promise]).finally(() => { - clearTimeout(handleSoon); - if (handle) { - handle.close(); - } + Promise.all([timeout(800), progressStateModel.promise]).finally(() => { + clearTimeout(notificationTimeout); + notificationHandle?.close(); }); - return promise; + return progressStateModel.promise; } private withViewletProgress

, R = unknown>(viewletId: string, task: (progress: IProgress) => P, options: IProgressCompositeOptions): P { diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 1f6c9878fa5e..94a613186842 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { Emitter, AsyncEmitter } from 'vs/base/common/event'; -import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, ITextFileEditorModel, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent, ITextFileSaveOptions, ITextFileEditorModelManager, ISaveParticipant } from 'vs/workbench/services/textfile/common/textfiles'; +import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, ITextFileEditorModel, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent, ITextFileSaveOptions, ITextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textfiles'; import { IRevertOptions, IEncodingSupport } from 'vs/workbench/common/editor'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files'; @@ -33,8 +33,6 @@ import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel' import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { coalesce } from 'vs/base/common/arrays'; import { suggestFilename } from 'vs/base/common/mime'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { isValidBasename } from 'vs/base/common/extpath'; @@ -59,18 +57,6 @@ export abstract class AbstractTextFileService extends Disposable implements ITex readonly untitled: IUntitledTextEditorModelManager = this.untitledTextEditorService; - saveErrorHandler = (() => { - const notificationService = this.notificationService; - - return { - onSaveError(error: Error, model: ITextFileEditorModel): void { - notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", model.name, toErrorMessage(error, false))); - } - }; - })(); - - saveParticipant: ISaveParticipant | undefined = undefined; - abstract get encoding(): IResourceEncodings; constructor( @@ -86,7 +72,6 @@ export abstract class AbstractTextFileService extends Disposable implements ITex @IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService, @ITextModelService private readonly textModelService: ITextModelService, @ICodeEditorService private readonly codeEditorService: ICodeEditorService, - @INotificationService private readonly notificationService: INotificationService, @IRemotePathService private readonly remotePathService: IRemotePathService ) { super(); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index fb425e6ac9f3..5b622561b3ad 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -79,7 +79,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private lastResolvedFileStat: IFileStatWithMetadata | undefined; private readonly saveSequentializer = new TaskSequentializer(); - private lastSaveAttemptTime = 0; private dirty = false; private inConflictMode = false; @@ -553,16 +552,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return false; // if model is in save conflict or error, do not save unless save reason is explicit } + // Actually do save and log this.logService.trace('[text file model] save() - enter', this.resource.toString()); - await this.doSave(options); - this.logService.trace('[text file model] save() - exit', this.resource.toString()); return true; } - private doSave(options: ITextFileSaveOptions): Promise { + private async doSave(options: ITextFileSaveOptions): Promise { if (typeof options.reason !== 'number') { options.reason = SaveReason.EXPLICIT; } @@ -587,7 +585,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil if (!options.force && !this.dirty) { this.logService.trace(`[text file model] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource.toString()); - return Promise.resolve(); + return; } // Return if currently saving by storing this save request as the next save that should happen. @@ -618,26 +616,34 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.textEditorModel.pushStackElement(); } - // A save participant can still change the model now and since we are so close to saving - // we do not want to trigger another auto save or similar, so we block this - // In addition we update our version right after in case it changed because of a model change - // - // Save participants can also be skipped through API. - const saveParticipantCancellation = new CancellationTokenSource(); - let saveParticipantPromise: Promise = Promise.resolve(versionId); - if (this.isResolved() && this.textFileService.saveParticipant && !options.skipSaveParticipants) { - const onCompleteOrError = () => { - this.ignoreDirtyOnModelContentChange = false; + const saveCancellation = new CancellationTokenSource(); - return this.versionId; - }; + return this.saveSequentializer.setPending(versionId, (async () => { - this.ignoreDirtyOnModelContentChange = true; - saveParticipantPromise = this.textFileService.saveParticipant.participate(this, { reason: options.reason }, saveParticipantCancellation.token).then(onCompleteOrError, onCompleteOrError); - } + // A save participant can still change the model now and since we are so close to saving + // we do not want to trigger another auto save or similar, so we block this + // In addition we update our version right after in case it changed because of a model change + // + // Save participants can also be skipped through API. + if (this.isResolved() && !options.skipSaveParticipants) { + try { + await this.textFileService.files.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token); + } catch (error) { + this.logService.error(`[text file model] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString()); + } + } - // mark the save participant as current pending save operation - return this.saveSequentializer.setPending(versionId, saveParticipantPromise.then(newVersionId => { + // It is possible that a subsequent save is cancelling this + // running save. As such we return early when we detect that + // However, we do not pass the token into the file service + // because that is an atomic operation currently without + // cancellation support, so we dispose the cancellation if + // it was not cancelled yet. + if (saveCancellation.token.isCancellationRequested) { + return; + } else { + saveCancellation.dispose(); + } // We have to protect against being disposed at this point. It could be that the save() operation // was triggerd followed by a dispose() operation right after without waiting. Typically we cannot @@ -661,32 +667,39 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // - the model is not dirty (otherwise we know there are changed which needs to go to the file) // - the model is not in orphan mode (because in that case we know the file does not exist on disk) // - the model version did not change due to save participants running - if (options.force && !this.dirty && !this.inOrphanMode && options.reason === SaveReason.EXPLICIT && versionId === newVersionId) { - return this.doTouch(newVersionId, options.reason); + if (options.force && !this.dirty && !this.inOrphanMode && options.reason === SaveReason.EXPLICIT && versionId === this.versionId) { + return this.doTouch(this.versionId, options.reason); } // update versionId with its new value (if pre-save changes happened) - versionId = newVersionId; + versionId = this.versionId; // Clear error flag since we are trying to save again this.inErrorMode = false; - // Remember when this model was saved last - this.lastSaveAttemptTime = Date.now(); - - // Save to Disk - // mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering) + // Save to Disk. We mark the save operation as currently pending with + // the latest versionId because it might have changed from a save + // participant triggering this.logService.trace(`[text file model] doSave(${versionId}) - before write()`, this.resource.toString()); const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); - return this.saveSequentializer.setPending(newVersionId, this.textFileService.write(lastResolvedFileStat.resource, this.createSnapshot(), { - overwriteReadonly: options.overwriteReadonly, - overwriteEncoding: options.overwriteEncoding, - mtime: lastResolvedFileStat.mtime, - encoding: this.getEncoding(), - etag: (options.ignoreModifiedSince || !this.filesConfigurationService.preventSaveConflicts(lastResolvedFileStat.resource, this.getMode())) ? ETAG_DISABLED : lastResolvedFileStat.etag, - writeElevated: options.writeElevated - }).then(stat => this.handleSaveSuccess(stat, versionId, options), error => this.handleSaveError(error, versionId, options))); - }), () => saveParticipantCancellation.cancel()); + const textFileEdiorModel = this; + return this.saveSequentializer.setPending(versionId, (async () => { + try { + const stat = await this.textFileService.write(lastResolvedFileStat.resource, textFileEdiorModel.createSnapshot(), { + overwriteReadonly: options.overwriteReadonly, + overwriteEncoding: options.overwriteEncoding, + mtime: lastResolvedFileStat.mtime, + encoding: this.getEncoding(), + etag: (options.ignoreModifiedSince || !this.filesConfigurationService.preventSaveConflicts(lastResolvedFileStat.resource, textFileEdiorModel.getMode())) ? ETAG_DISABLED : lastResolvedFileStat.etag, + writeElevated: options.writeElevated + }); + + this.handleSaveSuccess(stat, versionId, options); + } catch (error) { + this.handleSaveError(error, versionId, options); + } + })()); + })(), () => saveCancellation.cancel()); } private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: ITextFileSaveOptions): void { @@ -725,7 +738,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } // Show to user - this.textFileService.saveErrorHandler.onSaveError(error, this); + this.textFileService.files.saveErrorHandler.onSaveError(error, this); // Emit as event this._onDidSaveError.fire(); @@ -733,19 +746,24 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private doTouch(this: TextFileEditorModel & IResolvedTextFileEditorModel, versionId: number, reason: SaveReason): Promise { const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); - return this.saveSequentializer.setPending(versionId, this.textFileService.write(lastResolvedFileStat.resource, this.createSnapshot(), { - mtime: lastResolvedFileStat.mtime, - encoding: this.getEncoding(), - etag: lastResolvedFileStat.etag - }).then(stat => { - // Updated resolved stat with updated stat since touching it might have changed mtime - this.updateLastResolvedFileStat(stat); + return this.saveSequentializer.setPending(versionId, (async () => { + try { + const stat = await this.textFileService.write(lastResolvedFileStat.resource, this.createSnapshot(), { + mtime: lastResolvedFileStat.mtime, + encoding: this.getEncoding(), + etag: lastResolvedFileStat.etag + }); - // Emit File Saved Event - this._onDidSave.fire(reason); + // Updated resolved stat with updated stat since touching it might have changed mtime + this.updateLastResolvedFileStat(stat); - }, error => onUnexpectedError(error) /* just log any error but do not notify the user since the file was not dirty */)); + // Emit File Saved Event + this._onDidSave.fire(reason); + } catch (error) { + onUnexpectedError(error); // just log any error but do not notify the user since the file was not dirty + } + })()); } private updateSavedVersionId(): void { @@ -776,10 +794,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#endregion - getLastSaveAttemptTime(): number { - return this.lastSaveAttemptTime; - } - hasState(state: ModelState): boolean { switch (state) { case ModelState.CONFLICT: diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 337797b264b1..a3a6cdba9379 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { dispose, IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ITextFileEditorModel, ITextFileEditorModelManager, IModelLoadOrCreateOptions, ITextFileModelLoadEvent, ITextFileModelSaveEvent } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileEditorModel, ITextFileEditorModelManager, IModelLoadOrCreateOptions, ITextFileModelLoadEvent, ITextFileModelSaveEvent, ITextFileSaveParticipant, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; @@ -15,6 +17,10 @@ import { IFileService, FileChangesEvent } from 'vs/platform/files/common/files'; import { distinct, coalesce } from 'vs/base/common/arrays'; import { ResourceQueue } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { TextFileSaveParticipant } from 'vs/workbench/services/textfile/common/textFileSaveParticipant'; +import { SaveReason } from 'vs/workbench/common/editor'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager { @@ -39,6 +45,16 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE private readonly _onDidChangeOrphaned = this._register(new Emitter()); readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event; + saveErrorHandler = (() => { + const notificationService = this.notificationService; + + return { + onSaveError(error: Error, model: ITextFileEditorModel): void { + notificationService.error(localize('genericSaveError', "Failed to save '{0}': {1}", model.name, toErrorMessage(error, false))); + } + }; + })(); + private readonly mapResourceToModel = new ResourceMap(); private readonly mapResourceToModelListeners = new ResourceMap(); private readonly mapResourceToDisposeListener = new ResourceMap(); @@ -49,7 +65,8 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE constructor( @ILifecycleService private readonly lifecycleService: ILifecycleService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IFileService private readonly fileService: IFileService + @IFileService private readonly fileService: IFileService, + @INotificationService private readonly notificationService: INotificationService ) { super(); @@ -84,7 +101,13 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // to have a size of 2 (1 running load and 1 queued load). const queue = this.modelLoadQueue.queueFor(model.resource); if (queue.size <= 1) { - queue.queue(() => model.load().then(undefined, onUnexpectedError)); + queue.queue(async () => { + try { + await model.load(); + } catch (error) { + onUnexpectedError(error); + } + }); } } @@ -221,6 +244,20 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE } } + //#region Save participants + + private readonly saveParticipants = this._register(this.instantiationService.createInstance(TextFileSaveParticipant)); + + addSaveParticipant(participant: ITextFileSaveParticipant): IDisposable { + return this.saveParticipants.addSaveParticipant(participant); + } + + runSaveParticipants(model: IResolvedTextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise { + return this.saveParticipants.participate(model, context, token); + } + + //#endregion + clear(): void { // model caches diff --git a/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts b/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts new file mode 100644 index 000000000000..2ba91174b679 --- /dev/null +++ b/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { raceCancellation } from 'vs/base/common/async'; +import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; +import { localize } from 'vs/nls'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { ITextFileSaveParticipant, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; +import { SaveReason } from 'vs/workbench/common/editor'; +import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; + +export class TextFileSaveParticipant extends Disposable { + + private readonly saveParticipants: ITextFileSaveParticipant[] = []; + + constructor( + @IProgressService private readonly progressService: IProgressService, + @ILogService private readonly logService: ILogService + ) { + super(); + } + + addSaveParticipant(participant: ITextFileSaveParticipant): IDisposable { + this.saveParticipants.push(participant); + + return toDisposable(() => this.saveParticipants.splice(this.saveParticipants.indexOf(participant), 1)); + } + + participate(model: IResolvedTextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise { + const cts = new CancellationTokenSource(token); + + return this.progressService.withProgress({ + title: localize('saveParticipants', "Running Save Participants for '{0}'", model.name), + location: ProgressLocation.Notification, + cancellable: true, + delay: model.isDirty() ? 3000 : 5000 + }, async progress => { + + // undoStop before participation + model.textEditorModel.pushStackElement(); + + for (const saveParticipant of this.saveParticipants) { + if (cts.token.isCancellationRequested) { + break; + } + + try { + const promise = saveParticipant.participate(model, context, progress, cts.token); + await raceCancellation(promise, cts.token); + } catch (err) { + this.logService.warn(err); + } + } + + // undoStop after participation + model.textEditorModel.pushStackElement(); + }, () => { + // user cancel + cts.dispose(true); + }); + } + + dispose(): void { + this.saveParticipants.splice(0, this.saveParticipants.length); + } +} diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index c3c1f68d02ca..c9532133e000 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -17,6 +17,7 @@ import { isNative } from 'vs/base/common/platform'; import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; export const ITextFileService = createDecorator('textFileService'); @@ -51,17 +52,6 @@ export interface ITextFileService extends IDisposable { */ readonly encoding: IResourceEncodings; - /** - * The handler that should be called when saving fails. Can be overridden - * to handle save errors in a custom way. - */ - saveErrorHandler: ISaveErrorHandler; - - /** - * The save participant if any. By default, no save participant is registered. - */ - saveParticipant: ISaveParticipant | undefined; - /** * A resource is dirty if it has unsaved changes or is an untitled file not yet saved. * @@ -226,14 +216,6 @@ export interface ISaveErrorHandler { onSaveError(error: Error, model: ITextFileEditorModel): void; } -export interface ISaveParticipant { - - /** - * Participate in a save of a model. Allows to change the model before it is being saved to disk. - */ - participate(model: IResolvedTextFileEditorModel, context: { reason: SaveReason }, token: CancellationToken): Promise; -} - /** * States the text file editor model can be in. */ @@ -357,6 +339,20 @@ export interface ITextFileModelLoadEvent { reason: LoadReason; } +export interface ITextFileSaveParticipant { + + /** + * Participate in a save of a model. Allows to change the model + * before it is being saved to disk. + */ + participate( + model: IResolvedTextFileEditorModel, + context: { reason: SaveReason }, + progress: IProgress, + token: CancellationToken + ): Promise; +} + export interface ITextFileEditorModelManager { readonly onDidLoad: Event; @@ -372,6 +368,11 @@ export interface ITextFileEditorModelManager { resolve(resource: URI, options?: IModelLoadOrCreateOptions): Promise; + addSaveParticipant(participant: ITextFileSaveParticipant): IDisposable; + runSaveParticipants(model: IResolvedTextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise + + saveErrorHandler: ISaveErrorHandler; + disposeModel(model: ITextFileEditorModel): void; } diff --git a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts index 34ffc230de40..def5df7ccbe8 100644 --- a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts @@ -37,7 +37,6 @@ import { assign } from 'vs/base/common/objects'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; export class NativeTextFileService extends AbstractTextFileService { @@ -56,10 +55,9 @@ export class NativeTextFileService extends AbstractTextFileService { @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @ITextModelService textModelService: ITextModelService, @ICodeEditorService codeEditorService: ICodeEditorService, - @INotificationService notificationService: INotificationService, @IRemotePathService remotePathService: IRemotePathService ) { - super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, dialogService, fileDialogService, textResourceConfigurationService, filesConfigurationService, textModelService, codeEditorService, notificationService, remotePathService); + super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, dialogService, fileDialogService, textResourceConfigurationService, filesConfigurationService, textModelService, codeEditorService, remotePathService); } private _encoding: EncodingOracle | undefined; diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index 61bddf39309e..af2880b2e160 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -120,7 +120,6 @@ suite('Files - TextFileEditorModel', () => { await pendingSave; - assert.ok(model.getLastSaveAttemptTime() <= Date.now()); assert.ok(model.hasState(ModelState.SAVED)); assert.ok(!model.isDirty()); assert.ok(savedEvent); @@ -488,8 +487,6 @@ suite('Files - TextFileEditorModel', () => { assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt'))); assert.ok(assertIsDefined(model1.getStat()).mtime > m1Mtime); assert.ok(assertIsDefined(model2.getStat()).mtime > m2Mtime); - assert.ok(model1.getLastSaveAttemptTime() > m1Mtime); - assert.ok(model2.getLastSaveAttemptTime() > m2Mtime); model1.dispose(); model2.dispose(); @@ -505,56 +502,98 @@ suite('Files - TextFileEditorModel', () => { eventCounter++; }); - accessor.textFileService.saveParticipant = { - participate: model => { + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: async model => { assert.ok(model.isDirty()); model.textEditorModel!.setValue('bar'); assert.ok(model.isDirty()); eventCounter++; - return Promise.resolve(); } - }; + }); await model.load(); model.textEditorModel!.setValue('foo'); await model.save(); - model.dispose(); assert.equal(eventCounter, 2); + + participant.dispose(); + model.textEditorModel!.setValue('bar'); + + await model.save(); + assert.equal(eventCounter, 3); + + model.dispose(); + }); + + test('Save Participant - skip', async function () { + let eventCounter = 0; + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: async model => { + eventCounter++; + } + }); + + await model.load(); + model.textEditorModel!.setValue('foo'); + + await model.save({ skipSaveParticipants: true }); + assert.equal(eventCounter, 0); + + participant.dispose(); + model.dispose(); }); test('Save Participant, async participant', async function () { + let eventCounter = 0; const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); - accessor.textFileService.saveParticipant = { - participate: (model) => { + model.onDidSave(e => { + assert.ok(!model.isDirty()); + eventCounter++; + }); + + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: model => { + assert.ok(model.isDirty()); + model.textEditorModel!.setValue('bar'); + assert.ok(model.isDirty()); + eventCounter++; + return timeout(10); } - }; + }); await model.load(); model.textEditorModel!.setValue('foo'); const now = Date.now(); await model.save(); + assert.equal(eventCounter, 2); assert.ok(Date.now() - now >= 10); + model.dispose(); + participant.dispose(); }); test('Save Participant, bad participant', async function () { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); - accessor.textFileService.saveParticipant = { - participate: (model) => { - return Promise.reject(new Error('boom')); + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: async model => { + new Error('boom'); } - }; + }); await model.load(); model.textEditorModel!.setValue('foo'); await model.save(); + model.dispose(); + participant.dispose(); }); test('Save Participant, participant cancelled when saved again', async function () { @@ -562,13 +601,15 @@ suite('Files - TextFileEditorModel', () => { let participations: boolean[] = []; - accessor.textFileService.saveParticipant = { - participate: (model) => { - return timeout(10).then(() => { + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: async (model, context, progress, token) => { + await timeout(10); + + if (!token.isCancellationRequested) { participations.push(true); - }); + } } - }; + }); await model.load(); @@ -579,11 +620,62 @@ suite('Files - TextFileEditorModel', () => { const p2 = model.save(); model.textEditorModel!.setValue('foo 2'); - await model.save(); + const p3 = model.save(); - await p1; - await p2; + model.textEditorModel!.setValue('foo 3'); + const p4 = model.save(); + + await Promise.all([p1, p2, p3, p4]); assert.equal(participations.length, 1); + + model.dispose(); + participant.dispose(); + }); + + test('Save Participant, calling save from within is unsupported but does not explode (sync save)', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await testSaveFromSaveParticipant(model, false); + model.dispose(); }); + + test('Save Participant, calling save from within is unsupported but does not explode (async save)', async function () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await testSaveFromSaveParticipant(model, true); + + model.dispose(); + }); + + async function testSaveFromSaveParticipant(model: TextFileEditorModel, async: boolean): Promise { + let savePromise: Promise; + let breakLoop = false; + + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: async model => { + if (breakLoop) { + return; + } + + breakLoop = true; + + if (async) { + await timeout(10); + } + const newSavePromise = model.save(); + + // assert that this is the same promise as the outer one + assert.equal(savePromise, newSavePromise); + } + }); + + await model.load(); + model.textEditorModel!.setValue('foo'); + + savePromise = model.save(); + await savePromise; + + participant.dispose(); + } }); diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index 14366966e16f..230a76fe556d 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -537,6 +537,22 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor } } + // If a value is not present in the cache, it must be reset to default + this.viewContainersRegistry.all.forEach(viewContainer => { + const viewDescriptorCollection = this.getViewDescriptors(viewContainer); + viewDescriptorCollection.allViewDescriptors.forEach(viewDescriptor => { + if (!newCachedPositions.has(viewDescriptor.id)) { + const currentContainer = this.getViewContainer(viewDescriptor.id); + const defaultContainer = this.getDefaultContainer(viewDescriptor.id); + if (currentContainer && defaultContainer && currentContainer !== defaultContainer) { + this.moveViews([viewDescriptor], currentContainer, defaultContainer); + } + + this.cachedViewInfo.delete(viewDescriptor.id); + } + }); + }); + this.cachedViewInfo = this.getCachedViewPositions(); } } @@ -571,6 +587,16 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor }); }); + // Do no save default positions to the cache + // so that default changes can be recognized + // https://github.com/microsoft/vscode/issues/90414 + for (const [viewId, containerInfo] of this.cachedViewInfo) { + const defaultContainer = this.getDefaultContainer(viewId); + if (defaultContainer?.id === containerInfo.containerId) { + this.cachedViewInfo.delete(viewId); + } + } + this.cachedViewPositionsValue = JSON.stringify([...this.cachedViewInfo]); } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 04ae11414128..50efaef1f8ab 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -89,6 +89,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { Direction } from 'vs/base/browser/ui/grid/grid'; +import { IProgressService, IProgressOptions, IProgressWindowOptions, IProgressNotificationOptions, IProgressCompositeOptions, IProgress, IProgressStep, emptyProgress } from 'vs/platform/progress/common/progress'; export import TestTextResourcePropertiesService = CommonWorkbenchTestServices.TestTextResourcePropertiesService; export import TestContextService = CommonWorkbenchTestServices.TestContextService; @@ -120,7 +121,6 @@ export class TestTextFileService extends BrowserTextFileService { @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @ITextModelService textModelService: ITextModelService, @ICodeEditorService codeEditorService: ICodeEditorService, - @INotificationService notificationService: INotificationService, @IRemotePathService remotePathService: IRemotePathService ) { super( @@ -136,7 +136,6 @@ export class TestTextFileService extends BrowserTextFileService { filesConfigurationService, textModelService, codeEditorService, - notificationService, remotePathService ); } @@ -175,6 +174,7 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i instantiationService.stub(IEnvironmentService, TestEnvironmentService); const contextKeyService = instantiationService.createInstance(MockContextKeyService); instantiationService.stub(IContextKeyService, contextKeyService); + instantiationService.stub(IProgressService, new TestProgressService()); const workspaceContextService = new TestContextService(TestWorkspace); instantiationService.stub(IWorkspaceContextService, workspaceContextService); const configService = new TestConfigurationService(); @@ -217,6 +217,19 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i return instantiationService; } +export class TestProgressService implements IProgressService { + + _serviceBrand: undefined; + + withProgress( + options: IProgressOptions | IProgressWindowOptions | IProgressNotificationOptions | IProgressCompositeOptions, + task: (progress: IProgress) => Promise, + onDidCancel?: ((choice?: number | undefined) => void) | undefined + ): Promise { + return task(emptyProgress); + } +} + export class TestAccessibilityService implements IAccessibilityService { _serviceBrand: undefined; @@ -865,6 +878,7 @@ export class TestHostService implements IHostService { _serviceBrand: undefined; readonly hasFocus: boolean = true; + async hadLastFocus(): Promise { return true; } readonly onDidChangeFocus: Event = Event.None; async restart(): Promise { } diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index b8a5e1b1ee1a..b95b7d7b1eb0 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -41,7 +41,7 @@ suite('Notifications', () => { // Events let called = 0; - item1.onDidExpansionChange(() => { + item1.onDidChangeExpansion(() => { called++; }); @@ -53,7 +53,7 @@ suite('Notifications', () => { assert.equal(called, 2); called = 0; - item1.onDidLabelChange(e => { + item1.onDidChangeLabel(e => { if (e.kind === NotificationViewItemLabelKind.PROGRESS) { called++; } @@ -65,7 +65,7 @@ suite('Notifications', () => { assert.equal(called, 2); called = 0; - item1.onDidLabelChange(e => { + item1.onDidChangeLabel(e => { if (e.kind === NotificationViewItemLabelKind.MESSAGE) { called++; } @@ -74,7 +74,7 @@ suite('Notifications', () => { item1.updateMessage('message update'); called = 0; - item1.onDidLabelChange(e => { + item1.onDidChangeLabel(e => { if (e.kind === NotificationViewItemLabelKind.SEVERITY) { called++; } @@ -83,7 +83,7 @@ suite('Notifications', () => { item1.updateSeverity(Severity.Error); called = 0; - item1.onDidLabelChange(e => { + item1.onDidChangeLabel(e => { if (e.kind === NotificationViewItemLabelKind.ACTIONS) { called++; } @@ -146,12 +146,12 @@ suite('Notifications', () => { const model = new NotificationsModel(); let lastNotificationEvent!: INotificationChangeEvent; - model.onDidNotificationChange(e => { + model.onDidChangeNotification(e => { lastNotificationEvent = e; }); let lastStatusMessageEvent!: IStatusMessageChangeEvent; - model.onDidStatusMessageChange(e => { + model.onDidChangeStatusMessage(e => { lastStatusMessageEvent = e; }); diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 0857cc45c11b..db2309ddf7b8 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -22,7 +22,6 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { URI } from 'vs/base/common/uri'; import { IReadTextFileOptions, ITextFileStreamContent, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; @@ -62,7 +61,6 @@ export class TestTextFileService extends NativeTextFileService { @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @ITextModelService textModelService: ITextModelService, @ICodeEditorService codeEditorService: ICodeEditorService, - @INotificationService notificationService: INotificationService, @IRemotePathService remotePathService: IRemotePathService ) { super( @@ -79,7 +77,6 @@ export class TestTextFileService extends NativeTextFileService { filesConfigurationService, textModelService, codeEditorService, - notificationService, remotePathService ); } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 9debc489ee12..1e943601b665 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -272,6 +272,9 @@ import 'vs/workbench/contrib/userDataSync/browser/userDataSync.contribution'; // Code Actions import 'vs/workbench/contrib/codeActions/common/codeActions.contribution'; +// Welcome +import 'vs/workbench/contrib/welcome/common/viewsWelcome.contribution'; + // Timeline import 'vs/workbench/contrib/timeline/browser/timeline.contribution'; diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 4cd646930bb5..140711b300ea 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -80,17 +80,29 @@ interface IApplicationLink { uri: URI; /** - * A label for the link to display. + * A label for the application link to display. */ label: string; } -interface IApplicationLinkProvider { - (): IApplicationLink[] | undefined +interface ICommand { + + /** + * An identifier for the command. Commands can be executed from extensions + * using the `vscode.commands.executeCommand` API using that command ID. + */ + id: string, + + /** + * A function that is being executed with any arguments passed over. + */ + handler: (...args: any[]) => void; } interface IWorkbenchConstructionOptions { + //#region Connection related configuration + /** * The remote authority is the IP:PORT from where the workbench is served * from. It is for example being used for the websocket connections as address. @@ -108,6 +120,36 @@ interface IWorkbenchConstructionOptions { */ readonly webviewEndpoint?: string; + /** + * A factory for web sockets. + */ + readonly webSocketFactory?: IWebSocketFactory; + + /** + * A provider for resource URIs. + */ + readonly resourceUriProvider?: IResourceUriProvider; + + /** + * Resolves an external uri before it is opened. + */ + readonly resolveExternalUri?: IExternalUriResolver; + + /** + * Support for creating tunnels. + */ + readonly tunnelFactory?: ITunnelFactory; + + /** + * Support for filtering candidate ports + */ + readonly showCandidate?: IShowCandidate; + + //#endregion + + + //#region Workbench configuration + /** * A handler for opening workspaces and providing the initial workspace. */ @@ -119,16 +161,6 @@ interface IWorkbenchConstructionOptions { */ userDataProvider?: IFileSystemProvider; - /** - * A factory for web sockets. - */ - readonly webSocketFactory?: IWebSocketFactory; - - /** - * A provider for resource URIs. - */ - readonly resourceUriProvider?: IResourceUriProvider; - /** * The credentials provider to store and retrieve secrets. */ @@ -154,21 +186,6 @@ interface IWorkbenchConstructionOptions { */ readonly resolveCommonTelemetryProperties?: ICommontTelemetryPropertiesResolver; - /** - * Resolves an external uri before it is opened. - */ - readonly resolveExternalUri?: IExternalUriResolver; - - /** - * Support for creating tunnels. - */ - readonly tunnelFactory?: ITunnelFactory; - - /** - * Support for filtering candidate ports - */ - readonly showCandidate?: IShowCandidate; - /** * Provide entries for the "Open in Desktop" feature. * @@ -179,7 +196,20 @@ interface IWorkbenchConstructionOptions { * - N elements: there will be a "Open in Desktop" affordance that opens * a picker on click to select which application to open. */ - readonly applicationLinkProvider?: IApplicationLinkProvider; + readonly applicationLinks?: readonly IApplicationLink[]; + + /** + * A set of optional commands that should be registered with the commands + * registry. + * + * Note: commands can be called from extensions if the identifier is known! + */ + readonly commands?: readonly ICommand[]; + + //#endregion + + + //#region Diagnostics /** * Current logging level. Default is `LogLevel.Info`. @@ -190,20 +220,8 @@ interface IWorkbenchConstructionOptions { * Whether to enable the smoke test driver. */ readonly driver?: boolean; -} -interface ICommandHandler { - (...args: any[]): void; -} - -interface IWorkbench { - - /** - * Register a command with the provided identifier and handler with - * the workbench. The command can be called from extensions using the - * `vscode.commands.executeCommand` API. - */ - registerCommand(id: string, command: ICommandHandler): IDisposable; + //#endregion } /** @@ -211,24 +229,22 @@ interface IWorkbench { * * @param domElement the container to create the workbench in * @param options for setting up the workbench - * - * @returns the workbench facade with additional methods to call on. */ -async function create(domElement: HTMLElement, options: IWorkbenchConstructionOptions): Promise { +async function create(domElement: HTMLElement, options: IWorkbenchConstructionOptions): Promise { // Startup workbench await main(domElement, options); - // Return facade - return { - registerCommand: (id: string, command: ICommandHandler): IDisposable => { - return CommandsRegistry.registerCommand(id, (accessor, ...args: any[]) => { + // Register commands if any + if (Array.isArray(options.commands)) { + for (const command of options.commands) { + CommandsRegistry.registerCommand(command.id, (accessor, ...args: any[]) => { // we currently only pass on the arguments but not the accessor // to the command to reduce our exposure of internal API. - command(...args); + command.handler(...args); }); } - }; + } } export { @@ -237,9 +253,6 @@ export { create, IWorkbenchConstructionOptions, - // Workbench Facade - IWorkbench, - ICommandHandler, // Basic Types URI, @@ -291,5 +304,7 @@ export { // Protocol Links IApplicationLink, - IApplicationLinkProvider + + // Commands + ICommand }; diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index c18e01808d3b..40ee4c82851e 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -141,10 +141,12 @@ export function connect(engine: 'chromium' | 'webkit' | 'firefox' = 'chromium'): return new Promise(async (c) => { const browser = await playwright[engine].launch({ // Run in Edge dev on macOS - // executablePath: '/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev' + // executablePath: '/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev', + headless: false }); - const page = (await browser.defaultContext().pages())[0]; - await page.setViewport({ width, height }); + const context = await browser.newContext(); + const page = await context.newPage(); + await page.setViewportSize({ width, height }); await page.goto(`${endpoint}&folder=vscode-remote://localhost:9888${URI.file(workspacePath!).path}`); const result = { client: { dispose: () => browser.close() && teardown() }, diff --git a/test/integration/browser/package.json b/test/integration/browser/package.json index d43ee230c465..beb4142c55dd 100644 --- a/test/integration/browser/package.json +++ b/test/integration/browser/package.json @@ -9,6 +9,7 @@ "devDependencies": { "@types/mkdirp": "0.5.1", "@types/node": "^12.11.7", + "@types/optimist": "0.0.29", "@types/rimraf": "2.0.2", "@types/tmp": "^0.1.0", "rimraf": "^2.6.1", diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 2ed75099d31f..7972dd765216 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -11,8 +11,9 @@ import * as tmp from 'tmp'; import * as rimraf from 'rimraf'; import { URI } from 'vscode-uri'; import * as kill from 'tree-kill'; +import * as optimistLib from 'optimist'; -const optimist = require('optimist') +const optimist = optimistLib .describe('workspacePath', 'path to the workspace to open in the test').string('workspacePath') .describe('extensionDevelopmentPath', 'path to the extension to test').string('extensionDevelopmentPath') .describe('extensionTestsPath', 'path to the extension tests').string('extensionTestsPath') @@ -28,11 +29,12 @@ if (optimist.argv.help) { const width = 1200; const height = 800; -async function runTestsInBrowser(browserType: string, endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise { +async function runTestsInBrowser(browserType: 'chromium' | 'firefox' | 'webkit', endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise { const args = process.platform === 'linux' && browserType === 'chromium' ? ['--no-sandbox'] : undefined; // disable sandbox to run chrome on certain Linux distros const browser = await playwright[browserType].launch({ headless: !Boolean(optimist.argv.debug), dumpio: true, args }); - const page = (await browser.defaultContext().pages())[0]; - await page.setViewport({ width, height }); + const context = await browser.newContext(); + const page = await context.newPage(); + await page.setViewportSize({ width, height }); const host = endpoint.host; const protocol = 'vscode-remote'; diff --git a/test/integration/browser/yarn.lock b/test/integration/browser/yarn.lock index 35884dcfc513..126da8138c5d 100644 --- a/test/integration/browser/yarn.lock +++ b/test/integration/browser/yarn.lock @@ -38,6 +38,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.26.tgz#213e153babac0ed169d44a6d919501e68f59dea9" integrity sha512-UmUm94/QZvU5xLcUlNR8hA7Ac+fGpO1EG/a8bcWVz0P0LqtxFmun9Y2bbtuckwGboWJIT70DoWq1r3hb56n3DA== +"@types/optimist@0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/optimist/-/optimist-0.0.29.tgz#a8873580b3a84b69ac1e687323b15fbbeb90479a" + integrity sha1-qIc1gLOoS2msHmhzI7Ffu+uQR5o= + "@types/rimraf@2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e" diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index 884cb5de6673..d6e15c5e28df 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -120,7 +120,8 @@ const testModules = (async function () { async function runTestsInBrowser(testModules, browserType) { const args = process.platform === 'linux' && browserType === 'chromium' ? ['--no-sandbox'] : undefined; // disable sandbox to run chrome on certain Linux distros const browser = await playwright[browserType].launch({ headless: !Boolean(argv.debug), dumpio: true, args }); - const page = (await browser.defaultContext().pages())[0] + const context = await browser.newContext(); + const page = await context.newPage(); const target = url.pathToFileURL(path.join(__dirname, 'renderer.html')); if (argv.build) { target.search = `?build=true`; diff --git a/yarn.lock b/yarn.lock index e55bdc53b9f8..e4f5accd3c77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5878,11 +5878,6 @@ mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.0.3: - version "2.4.4" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" - integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== - mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" @@ -6927,29 +6922,28 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -playwright-core@=0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-0.10.0.tgz#86699c9cc3e613d733e6635a54aceea1993013d5" - integrity sha512-yernA6yrrBhmb8M5eO6GZsJOrBKWOZszlu65Luz8LP7ryaDExN1sE9XjQBNbiwJ5Gfs8cehtAO7GfTDJt+Z2cQ== +playwright-core@=0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-0.11.0.tgz#a2372833f6ec4e7886c4409e3da93df997aee61b" + integrity sha512-9UPP/Max65PMiZJz9DNWB3ZRWtTlYlceLFnm6JO8aU7m6Vw3gwCvuSGoC5W69H67q98jH0VPSPp546+EnkiR2g== dependencies: debug "^4.1.0" extract-zip "^1.6.6" https-proxy-agent "^3.0.0" jpeg-js "^0.3.6" - mime "^2.0.3" pngjs "^3.4.0" progress "^2.0.3" proxy-from-env "^1.0.0" - rimraf "^2.6.1" + rimraf "^3.0.2" uuid "^3.4.0" ws "^6.1.0" -playwright@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-0.10.0.tgz#d37f7e42e0e868dcc4ec35cb0a8dbc6248457642" - integrity sha512-f3VRME/PIO5NbcWnlCDfXwPC0DAZJ7ETkcAdE+sensLCOkfDtLh97E71ZuxNCaPYsUA6FIPi5syD8pHJW/4hQQ== +playwright@0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-0.11.0.tgz#2abec99ea278b220bcd3902d7520ec22abc2d97e" + integrity sha512-cTJZ06OhwseMC9+D6KX1NmZXyEoaJl0o6GLkDhwmou3IFTrUFVOw7KYMBpcbJz0Rhb/de5ZPFlDTffLfEy/9lg== dependencies: - playwright-core "=0.10.0" + playwright-core "=0.11.0" plist@^3.0.1: version "3.0.1" @@ -7952,6 +7946,13 @@ rimraf@^2.4.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2: dependencies: glob "^7.0.5" +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + rimraf@~2.2.6: version "2.2.8" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582"